Skip to main content

0.89.0

Breaking changes

  • The deprecated agent.discovery.labelTagKeys setting has been removed. Labels for discovered resources come from the plugin’s own label query, which every resource plugin ships a default for. If your formae.conf.pkl still sets labelTagKeys, remove it, or the agent will fail to read its configuration on startup. If you relied on it to label resources from a tag other than the plugin’s default, set that plugin’s labelConfig.defaultQuery to a JSONPath query naming the tag you want, for example $.Tags[?(@.Key=='Owner')].Value. Where the query finds nothing, discovered resources keep the provider’s own identifier as their label, as before.
  • The formae.resource.errors metric has been retired, along with the “Resource Errors” panel in formae status and the ResourceErrors field in the agent API. The gauge tried to report how many resources are failing right now by reconstructing that from the event log on every scrape, which cannot be done reliably: a failed create leaves no resource row and takes a new identifier on each retry, so “still declared, failing to create” and “attempted once, then abandoned” are indistinguishable. In its place the agent emits formae.resource.operation.failures, a counter incremented where the failure happens, labelled by resource_type, operation, plugin, and the failure_stage the operation reached (resolving, synchronizing, creating, deleting, and so on). Create failures are counted, which the gauge could not do. Rejections are not, since a rejected update is not a failed one. The signal is a different shape and it is worth knowing which questions it answers. It reports what failed over a window rather than what is broken at this instant, it counts attempts rather than resources, and it lives in the agent’s memory, so a restart resets it. If you alert on the old gauge, move to the new counter before upgrading. There is no longer a per-resource-type view of what is currently broken; the command-level failure count is the closest equivalent until one is rebuilt.
  • Reconcile now manages a stack’s inline policies exactly. Applying a forma in reconcile mode removes any inline policy that the declared stack no longer carries, the same way reconcile already treats resources and standalone policy attachments. Re-applying the stack without a policy is now the supported way to remove it. If you added inline policies out of band (for example through the MCP server) to a stack that a forma declares, the next reconcile of that stack deletes them; declare them in the forma to keep them. Patch mode is unaffected and never touches policies you don’t mention.

New features and improvements

  • First-class secret management. A secret is now an ordinary managed resource whose value you can reference anywhere. Write secret.res.secretValue to use it, secret.res.secretValue.at("key") to pull one entry out of a map-shaped secret such as a Kubernetes Secret’s data, and .json("path") to reach into a JSON payload. The value is read live from the provider at every plugin call, so onboarding a new credential or rotating an existing one takes effect without restarting the agent. A resource or target that references a secret stores only the reference; the secret’s own value is hashed at rest and never written in cleartext. References work both in a resource’s properties (for example a database password) and in a target’s configuration (for example an API token), and resolve during discovery and destroy as well as apply.
  • formae generates and rotates credentials. Instead of writing a password into your forma (or minting one at evaluation time and pinning it with setOnce), declare a generator and bind resource properties to its output. formae draws the value, delivers it to every bound property, and, when the generator declares a rotation cadence, rotates it on schedule, unattended:
    Two generator kinds are available. PasswordGenerator draws a random password. KeyPairGenerator draws an RSA key pair as two named outputs of one draw, gen.privateKey (PKCS#8 PEM) and gen.publicKey (PKIX PEM), so two destinations bound to the two halves always hold halves of the same pair; bits is 2048 (the default), 3072 or 4096. A property binds to the output its envelope names, and a binding that names an output the generator’s kind does not produce fails the apply with the destination and output named. Once a key-pair generator is stored, the agent cannot be downgraded to a version that predates it. The drawn value never appears anywhere: not in the plan, the inventory, the logs, or the datastore, where only a digest is kept. A rotation is one coordinated command that moves the secret and everything that consumes it by reference, transitively: a database role whose password references the secret, and the database owned by that role, are updated in the same command, so the credential and the systems using it move together. Rotation refuses on a drifted stack (or a drifted consumer) the way any update does; a stack carrying an auto-reconcile policy is the standing opt-in to overwrite drift. Omit rotation to draw once and never rotate. Inspect what rotates, when it last did, and what each generator feeds with formae inventory generators. The rotation cadence floor is fifteen minutes: AWS Secrets Manager retains a day of secret versions against a fixed quota, so a faster sustained cadence exhausts it within hours, and a credential that must turn over faster wants short-lived credentials issued per use. See Secrets and generated credentials for what to know before rotating a credential something reads to authenticate, including the brief window every rotation has.
  • Collections that formae and something else both write are no longer a standing conflict. Some fields have an ownership boundary running through the middle of them: Kubernetes metadata.labels and annotations, where a controller adds its own entries beside yours, or AWS attachment lists a service registers into. formae now tracks ownership per element rather than per field. Entries you declare are reconciled as usual, entries a co-actor wrote are left alone, and an entry you stop declaring is removed. Previously the whole field belonged to whoever wrote it last, so a co-managed collection either fought the other writer on every apply or had to be left out of the forma.
  • Destroying a resource that others depend on now stops and reports by default. When a resource you are destroying still has dependents, formae halts and tells you what depends on it instead of quietly removing those dependents along with it. Pass the cascade option when you mean to tear down the whole dependency chain.
  • The AWS plugin discovers far more of your existing infrastructure. With the AWS plugin ≥ 0.1.17, fifteen more resource types appear in discovery and can be brought under management, including IAM users and customer managed policies, KMS aliases, Lambda permissions, load balancer listener rules, API Gateway resources and methods, CloudFront distributions, EC2 routes, and EC2 prefix lists. Discovery keeps AWS-managed inventory out of your view: AWS-owned managed policies, reserved alias/aws/ aliases, AWS-owned prefix lists, and the implicit local route in every route table are excluded, so what you see is what you can actually manage. The supported resources page reflects the current discovery status of every type.
  • formae status split into formae command (with status and list subcommands) and formae agent status. The old status verb did double duty under one flag surface: a single command by id, a query across many commands, and the agent’s own health. It is now three focused commands: formae command status for a single command (an optional positional id, defaulting to the most recently executed command), formae command list for a query-driven, potentially multi-result view (now defaulting to 50 results instead of 10), and formae agent status for the agent’s own health. formae status command and formae status agent still work as deprecated aliases for one release, but no longer appear in help output or shell completion, so update scripts and muscle memory using the table below:
  • TTL policies can carry an absolute deadline. A TTL policy now takes exactly one of ttl (a duration, as before) or expiresAt (an RFC 3339 timestamp in UTC, for example "2026-09-01T00:00:00Z"). Use expiresAt when a stack must be destroyed at a known instant, such as the end of a trial or a scheduled teardown window; re-applying the stack never moves an absolute deadline. Declaring both or neither fails validation when the forma is evaluated. See the TTL policy page for guidance on choosing between the two forms.
  • Out-of-band changes to cloud-defaulted properties are now treated as drift. Many properties are filled in by the cloud when your forma doesn’t declare them, such as a bucket’s default encryption or a key’s rotation setting, and omitting them can be a deliberate choice to rely on that default. Previously a change someone made to such a property outside formae was invisible to apply: a simulate of the unchanged forma said “no changes”, and a reconcile quietly dropped the drift record without the change ever being shown. Now the default formae observed when it created or last updated the resource is defended like a declared value: a reconcile is rejected showing the change, exactly as for drift on a declared property, and a forced reconcile reverts it. To accept the new value instead, declare the property in your forma. Values the cloud or another system fills in on its own, at creation or later, such as targets a load balancer registers at runtime, are not treated as drift and remain visible in formae drift only.
  • Empty values inside document-style properties now reach the provider exactly as written. Some properties are whole documents the provider owns the meaning of, where an empty object or list is itself the declaration. The clearest example is a Kubernetes custom resource spec: cert-manager selects an issuer type by which key is present, so selfSigned: {} is a complete, valid configuration. Previously formae cleaned empty objects and lists out of every property before writing, which turned such a spec into an empty document and made the provider reject or misread it. Plugins can now mark a property as carrying meaningful empty values, and formae preserves that property byte for byte through planning, comparison, and the write itself. The first plugin to use this is the Kubernetes plugin for custom resource specs; nothing changes for properties that are not marked.
  • The agent can read its own database password from AWS Secrets Manager. Set datastore.postgres.passwordSecretArn and the agent resolves its Postgres credential from the named secret for every new connection instead of holding one password for the life of the process. Rotating that credential then takes effect without restarting the agent, which makes the agent itself a well-behaved consumer of a rotated secret. An unreadable secret fails startup rather than surfacing later on some connection. See the configuration reference for details.
  • The Aurora Data API datastore accepts a custom endpoint. agent.datastore.auroraDataAPI.endpoint points the agent at a specific RDS Data API endpoint instead of the SDK’s default for the region, which is what you need to run against a local or non-default endpoint. Leave it empty for the default. Credentials still come from the standard AWS credential chain either way. The backend already honoured the setting, but the configuration file had no way to express it, so Pkl rejected the property.
  • The omarchy theme follows Omarchy 4 (Quattro) as well as Omarchy 3. Omarchy 4 keeps the active theme in a new location, replaces the numbered terminal colors in its colors.toml with named ones, and declares light mode inside that file rather than with a marker file beside it. formae reads either generation, so cli.theme = "omarchy" keeps following your desktop theme across the upgrade, switching themes still recolors a running formae inventory or status watch in place, and a light theme is still detected when cli.appearance is auto.

Bug fixes

  • FIXED: Out-of-band changes to properties the cloud fills in for you were reported by the drift API but hidden from apply and simulate. Asking for the drift showed the change; applying said there was nothing to do. Worse, a reconcile that carried other changes moved past the drift without ever displaying it, so it disappeared from the next drift report as well. Such changes now appear in the plan for both apply and simulate.
  • FIXED: formae update could delete the directory it was reached through, including everything else in it. formae works out where to install by taking the grandparent of the running binary’s path. A formae reached through a copy or a symlink somewhere else, such as /usr/local/bin, a package manager’s bin directory, or a build tree, resolved to that location instead of its own install tree. Finding no install there, update offered to create one, and accepting removed the directory and its contents before recreating it. The update then finished and reported success, so nothing indicated anything had been lost. Update now refuses to create an install tree in a directory it did not install, and says where it expected to find one.
  • FIXED: Applying one forma could destroy and recreate resources belonging to another. Changing a target’s configuration in a way that requires replacing the target deletes and recreates every resource on it, across every stack that uses that target. The check that runs before this looked only at the resources declared in the forma being applied, so a forma that touched nothing but its own new stack could cascade a delete and recreate onto an unrelated stack sharing the same target label. Where the affected stack held a Kubernetes CustomResourceDefinition, every custom resource of that type went with it. Such an apply is now refused, naming the stacks that would have been caught.
  • FIXED: A resource could be created in your cloud account with no record of it in formae, and the error said nothing about it. When the provider created the resource but formae then failed to record it, the operation was marked failed with an empty message. The object existed at the provider while formae held nothing for it, so a destroy reported success with nothing to remove and applying again failed because the resource already existed. Both points where this can happen now record what was created, so the failure names the object left behind and you can find it.
  • FIXED: Discovery filters did not do what they read as doing, and the ones shipped in the bootstrap examples had no effect at all. Three behaviors were wrong. A filter naming no resource types matched nothing, though writing only conditions is the natural way to say “exclude anything tagged like this”; it now applies to every type. A filter naming no conditions matched everything it was scoped to; it now excludes nothing. And a filter you configured replaced the rules a plugin ships for itself instead of adding to them; the two are now combined, so a plugin’s built-in exclusions survive your configuration. Expect a visible change on upgrade. An agent with configured discovery filters, including one deployed from the bootstrap examples, drops the matching unmanaged rows from its inventory on the first sync after upgrading. That is the filters finally doing what they were written to do, not resources disappearing.
  • FIXED: Using managed: in a command query crashed the CLI. The key belongs to resource queries, but the command grammar accepted it and then failed on the type of the value it was given. It is now reported as an unknown field, the same as any other key that does not apply there. managed: still works in resource and destroy queries.
  • FIXED: Asking for help wrote to your configuration. Usage text and the startup banner read the active profile so they can be drawn in your configured colors, and did so through a call that creates configuration when none exists. formae --help, any command’s --help, and even a mistyped command created a default profile and pointed the configuration store at it. Rendering help no longer touches configuration.
  • FIXED: A resource that failed to create before formae reached the provider reported only that it had failed, with no reason. Failures at that stage happen before the plugin runs, so there was no plugin error to report and the status output had nothing to show; the cause appeared only in the agent’s log. These failures now record why, and the reason shows up in the command status like any other.
  • FIXED: The formae logo fell back to its low-fidelity form inside a herdr pane, even where the terminal underneath could draw graphics. herdr’s server keeps running after the client that started it goes away, so a session first started over SSH leaves the SSH environment variables set in that server, and every pane it opens later inherits them, including panes you attach to locally. formae read those variables as “this is a remote session” and skipped the capability probe that would have found graphics support. formae now recognises a herdr pane and probes it. A tmux running inside a herdr pane still skips the probe, since the innermost multiplexer owns the terminal.
  • FIXED: Scrolling an inventory tab past its first page lost the cursor bar and the per-cell coloring, including the highlight that marks a resource as unmanaged. Both now survive scrolling.
  • FIXED: A discovery filter the agent could not evaluate stopped discovery altogether. A filter expression can be written correctly enough to be accepted and still fail when it runs, typically by asking for a field on a value that does not have one, and that failure brought down the whole discovery pass rather than being confined to the one filter. A filter that cannot be evaluated now excludes nothing, which is what an unparseable one already did, and the expression and the reason are logged so a filter that quietly stops matching is visible rather than silent.
  • FIXED: A brief loss of the agent’s database while work was in flight could leave the agent looking healthy but doing nothing. Failed writes during the outage could exhaust the internal supervision budget and stop the machinery that executes commands, synchronizes state, and runs rotations, while the API kept answering, so in-flight commands sat incomplete until someone restarted the agent by hand. The agent now exits when that machinery stops, so its process supervisor (ECS, systemd) restarts it; on startup it re-runs the interrupted commands, which brings its records back in line with what actually happened in the cloud. If you run the agent without a process supervisor, configure one; the bootstrap examples already run it as a container service that is restarted on exit.
  • FIXED: Adding a new property to an already-deployed resource was silently skipped when its value comes from a reference that can only be resolved while the command runs, such as another resource’s secret value or an output of a resource created in the same apply. The apply reported success, the property was never written, and re-applying made no difference. formae now plans the update and writes the resolved value during execution. Properties that the provider only accepts at creation are deliberately left out: a first-time declaration there would force a destroy and recreate, and formae never replaces a resource over a value it has not resolved yet.
  • FIXED: A property fed by a reference to another resource could differ forever from the value the cloud provider reports back. Providers often accept one form of an identifier and return another: you pass a key’s ARN, the provider echoes the bare key ID, or you pass a secret’s name and it echoes the full ARN. formae compared a freshly resolved reference against the echoed form, and the two could never match. On a property you can update in place, that planned a no-op update on every reconcile. On a property the provider only accepts at creation, it silently planned a destroy and recreate for any ordinary edit: changing a Lambda function URL’s CORS settings replaced the URL, and editing a Secrets Manager resource policy replaced the policy, leaving a window with no policy attached. formae now records the value it sent when it last wrote the resource and compares against that, so an unchanged reference produces no diff while a reference that genuinely points somewhere new still updates.
    The first reconcile after upgrading plans the same corrective change the previous version planned on every reconcile, including the destroy and recreate on properties that only accept a value at creation. That single pass records the baseline, after which the resource stops churning. Review a simulate before applying if any of your stacks are affected.
  • FIXED: Resources could be reported as failed while they were still being created successfully. When a cloud provider rate-limited a request, the plugin retried internally, and formae’s check for an unresponsive plugin gave up before those retries could finish. The resource was marked failed and the failure cascaded to everything depending on it, which was most visible as intermittent, unexplained failures when applying many resources at once. That check now takes its patience from the retry settings the plugin is actually running with, so a plugin still working inside its retry budget is no longer mistaken for a stalled one. A genuinely stuck plugin is still detected, after about 100 seconds on default settings rather than 40.
  • FIXED: Map keys containing dots were split apart in a resource’s stored properties. A label such as app.kubernetes.io/name was recorded both under its correct key and as a phantom nested app object, which showed up as an unexpected property when formae compared state after a create and produced malformed output from formae extract for that resource. Cloud state was never affected: the split existed only in what formae stored, and the first reconcile of an affected resource rewrites it correctly with no action from you. Kubernetes workloads were the most exposed, since nearly every one labels its pod template with app.kubernetes.io/* keys.
  • FIXED: Editing a stack’s description silently extended its TTL. The countdown is documented to run from the stack’s creation, but the expiry check read the timestamp of the stack’s latest version, which a description edit refreshes. The countdown now always measures from when the stack was created.
    A stack whose original deadline has already passed, and that stayed alive only because of this bug, is destroyed on the first expiry check after upgrading. If you rely on such stacks, remove or extend their TTL policies before upgrading. The agent log records the creation time and computed deadline for every stack it expires, so each destroy is traceable.
  • FIXED: A stack’s creation time, shown in the API stack listing and used for the inventory view’s TTL expiry display, reported the time of the stack’s last description edit instead of when the stack was created.
  • FIXED: A change to a value that other resources follow through a chain of references converged one hop per apply: each apply reported success while resources further down the chain kept old values. The whole chain now updates in a single apply. As part of this, a set of resources whose references form a cycle is rejected when the plan is generated, with an error naming the cycle; previous versions accepted such a cycle and silently resolved it to stale values. If an apply starts failing with a reference-cycle error, break the cycle by declaring one of the values as a literal.
  • FIXED: A resource referencing another resource’s property kept the old value when both changed in the same apply. The plan showed only the source change and apply reported success; the referencing resource caught up only on the next apply. References now resolve against the value the source will hold after the command, so both update together.
  • FIXED: In reconcile mode, removing part of a resource’s declaration (for example a tag) could appear in the preview but be silently skipped during execution when the same resource also references another resource. The executed change now matches the plan you were shown.
  • FIXED: When a reference resolved during execution to a value that changes a property the provider only accepts at creation, the change was silently ignored. formae now fails that update with an error naming the affected fields instead of proceeding as if nothing changed, and it never performs a replacement the plan did not declare.
  • FIXED: Changing a property that the provider neither returns on read nor allows updating in place (declared both write-only and create-only in the plugin schema, for example a cluster’s access configuration) was silently ignored: no update, no replacement, and apply reported success while the cloud kept the old value. Such a change now plans the replacement it requires. On a resource brought in through discovery or import, where formae has never written the property, the declared value continues to be left alone.
  • FIXED: After formae absorbed an out-of-band deletion, references from other resources to the deleted resource still resolved to it. Updating such a resource failed with an internal error, and creating one was accepted only to fail during execution. Both now fail upfront with a clear error naming the missing referenced resource.
  • FIXED: A cascading destroy skipped the check for conflicting in-flight commands, so it could delete a resource that a concurrently running command was still creating. Cascading destroys now go through the same conflict check as every other command.
  • FIXED: Discovery could give two unmanaged resources the same label. The duplicate then stalled background synchronization: every subsequent sync command stayed in progress indefinitely until the agent was restarted. Discovered labels are now guaranteed unique, and a sync plans at most one update per resource.
  • FIXED: Replacing a resource while a background sync cycle was in flight could make formae lose track of it: a read planned before the replacement reported the old identity as gone, the record was removed, inventory no longer showed the resource, and the next simulate planned to create something that was already running. Reads made stale by a concurrent write are now detected and ignored.
  • FIXED: A secret declared as an opaque literal could appear in cleartext in the change preview when the resource holding it was updated as a consequence of a deleted or replaced dependency. That synthesized change is now redacted everywhere it appears: simulate output, the CLI, the stored changeset, and logs.
  • FIXED: Everything a plugin logged was recorded by the agent at error level regardless of the level the plugin chose, so routine warnings (for example a rejected credential) fired error-log alerts. Plugin log lines now keep the level the plugin assigned, including plugins that log in logfmt; a line naming no level on the error stream still defaults to error.
  • FIXED: Several formae extract and schema bugs produced wrong or unevaluable forma files. A field whose type is a union could extract as the wrong member, silently turning numbers into strings (or the reverse) and freezing a live cross-resource reference into a literal copy of its envelope; properties inherited through an intermediate schema base class were dropped from the output; a resource whose class extends an intermediate base was rejected or rendered into a huge unevaluable file; resolvables built on the specialised bases crashed serialization; and a plugin service directory whose name starts with “v” was mistaken for a schema version, breaking extraction for that plugin entirely. Extraction now renders these shapes correctly and fails loudly when it cannot render a resource faithfully, rather than writing incorrect code.
  • FIXED: TTL deadlines in the stacks view were shown in local time with nothing marking the timezone, while the stored deadline is a UTC instant. Deadlines are now rendered in UTC with a trailing Z, so the string reads the same for every operator and can be passed back to --expires-at without silently shifting the deadline by a UTC offset.
  • FIXED: The selected row in the interactive views could be nearly unreadable (light text on a light highlight) in the tokyo-night, gruvbox, and catppuccin-latte themes. The highlight is now derived from the theme background, so the selection stays legible with every theme and terminal palette.
  • FIXED: formae profile show failed with “not initialized” on a machine where formae had never run, and refused a configuration file from before profiles existed instead of migrating it. With no profile name it now resolves configuration the same way every other command does; naming a profile remains a pure read.
  • FIXED: Declaring an explicitly empty collection on a keyed property the cloud pre-populates was silently ignored. Writing tags = new Listing {} on a resource with live tags produced “no changes” in reconcile mode: the declared clear never ran, and entries added outside formae stayed invisible behind the empty declaration. An explicit empty declaration now means what it says: reconcile plans a removal for every live entry, and the simulate shows those removals before anything executes. A declaration like this that was previously inert now acts, so review the plan on stacks that carry one. Omitting the property entirely still leaves the cloud’s entries alone, and patch mode never removes anything.

0.88.1

New features and improvements

  • The inventory resources list loads quickly in large environments. Opening the resources tab in formae inventory no longer stalls for tens of seconds when a target holds tens of thousands of resources. The list now loads a lightweight summary of each resource (label, stack, type, and native ID) and fetches a resource’s full properties only when you open its row, so the first rows appear almost immediately no matter how large the environment is.
  • Destroy and simulate generate plans faster. Planning a destroy (including a simulate) used to slow down in proportion to the total number of resources in your datastore, because finding cross-stack dependents scanned the entire resources table. That lookup is now served by an index, so plan time scales with the resources actually involved rather than the size of the whole environment. On Postgres and Aurora, the first startup after upgrading runs a one-time, automatic backfill to populate the new index; it is idempotent and needs no action on your part. SQLite and MSSQL are unaffected.
  • Apply and destroy show every change at once. The simulation preview and the live progress view for formae apply and formae destroy used to show only the first several rows of each section (resources, targets, stacks, and policies) and hid the rest behind a “show more” prompt. The full list is now shown; scroll with the arrow keys to move through large plans.

Bug fixes

  • FIXED: Stray characters such as Gi=0,p=0;OK could appear in your shell after a formae command in some terminals. The startup banner no longer leaves anything behind in the shell.
  • FIXED: Canceling a command, or an agent restart at the wrong moment, could record a resource as created when it had not actually been provisioned, so your inventory showed a resource that did not exist in the cloud. formae now handles these timing situations without losing in-flight resource updates, so the recorded state matches what was actually applied.
  • FIXED: Progress bars in the status view could render at different widths from one row to the next, and on light or low-contrast themes the unfilled part of a bar could be invisible, making the bar look shorter than it was. Every progress bar now renders at the same width with its fill shown inside, and the pending portion is always visible.
  • FIXED: formae status --max-results ignored any value above 10. A requested limit greater than the default was silently reduced to 10; the value you pass is now honored across all storage backends.

0.88.0

New docs. The formae documentation has a new home and a refreshed structure. Two kinds of release notes now live outside these docs: the MCP server’s are in its CHANGELOG, and each plugin’s are on its hub page under the changelog tab (for example, the AWS plugin).

Breaking changes

  • Update your PklProject to the 0.88.0 formae schema. A PklProject pinned to an older formae schema version no longer evaluates against this release, because the schema the binary emits has changed. Bump the formae dependency in your PklProject to 0.88.0; formae extract detects a stale pin and walks you through the upgrade. The recommended way to author a forma is now extends "@formae/forma.pkl" with a typed Props class (see New features below); existing amends-based formae keep working once the project is on the 0.88.0 schema.

New features and improvements

  • The formae CLI got a facelift. The interactive views (status, inventory, drift, and the simulate/apply preview) have been redesigned for a cleaner, more consistent look, with a unified help overlay. The apply and destroy preview now ends in a prominent confirmation bar that summarizes the operation and asks you to confirm or abort before anything runs; a cascade destroy shows how many resources it will affect. Machine-readable output is unchanged.
  • Theme the formae CLI. The CLI is now themeable and ships with three built-in themes: quiet (the default), rich, and colorblind. Select one with cli.theme in your profile, or author your own as a TOML file in ~/.config/formae/themes/ and select it by name; see the CLI themes reference for the full palette, glyph, and behavior options. Every theme carries both a light and a dark color set, and cli.appearance picks which one to use, independently of the theme: auto (the default) detects the terminal background, while light and dark force a variant for terminals where detection is unreliable, such as under tmux or over SSH (the FORMAE_APPEARANCE environment variable overrides it for a single command). Setting cli.theme = "omarchy" instead derives the colors from your active Omarchy desktop theme (read from ~/.config/omarchy/current/theme/colors.toml) and follows its light or dark mode automatically; long-running views such as formae inventory and the status watch recolor live when you switch your desktop theme, and formae falls back to the default quiet theme if none is present.
  • Read-only plugin and update commands no longer ask for sudo, and a new formae refresh warms the package cache. formae plugin list, plugin search, plugin info, and update list now read the locally cached package index directly, so they run without sudo, and update list shows the newest available version even on a cold cache. formae refresh updates that cache for every configured repository across the stable and dev channels.
  • Declare a forma’s inputs as a typed class. A forma file can now use extends "@formae/forma.pkl" and define its inputs in a Props class, so property access is statically typed and Pkl type constraints validate CLI input before apply. A @formae.Flag { name = "..." } annotation maps a member such as certArn to a differently-cased flag such as --cert-arn. Existing amends-based formae keep working unchanged; extends is the recommended style going forward.
  • formae extract checks your project’s schema version. When you extract into a directory whose PklProject pins an older formae schema than the running binary, extract detects the mismatch and tells you how to upgrade (prompting on a TTY, or applying with --yes) instead of failing later with an opaque evaluation error. It also adds any missing plugin dependencies your extracted resources need.
  • formae cleans up inventory for targets that have been unreachable for a long time. If a target stays continuously unreachable for longer than a configurable period (24 hours by default), formae removes its discovered resources from inventory. It never deletes anything in the cloud, never affects resources under formae management, and never acts on a target with intermittent connectivity; re-applying the target restores its inventory. You can change the period or turn it off per target with the target’s reap field; see Unreachable targets.

Bug fixes

  • FIXED: In certain cases, secret values could be stored in plaintext even though formae treated them as redacted. Opaque secret properties (such as a Secrets Manager secret string or an RDS master password) are now stored as a one-way hash everywhere formae keeps state (apply, background sync, discovery, drift comparison, and extract), and are never written to logs. On the first startup after upgrading, a one-time backfill scrubs any secrets already stored in plaintext, including older resource versions, so existing data is redacted with no action on your part. A safeguard also stops a hashed value from being sent back to a provider in place of the real secret.
  • FIXED: random.id could crash when generating a long numeric ID. Asking for an ID of 20 or more digits aborted the command, and asking for 19 returned a value that was not uniformly distributed. random.id now returns a uniform value for lengths 1 through 18, and a clear error above 18 (a numeric ID is a 64-bit integer, which holds at most 18 digits). To generate longer values such as passwords, use random.password.
  • FIXED: Discovery filters did not clean up resources that were already in inventory. A discovery filter (agent.resourcePlugins[].discoveryFilters) prevents matching resources from being added to inventory, but it previously applied only to newly-scanned resources: anything discovered before the filter was added, or before it began matching the resource’s tags, stayed in formae inventory indefinitely. Background sync now re-checks existing unmanaged inventory entries against the active filters and removes the ones that match, so adding a filter also clears out the entries it would have excluded. Only unmanaged, discovered entries are affected: resources under formae management are never removed even when they match a filter, and the underlying cloud resources are left untouched.
  • FIXED: Startup warned about duplicate config directories on Linux. When XDG_CONFIG_HOME was set to $HOME/.config (a common Linux default), formae printed a spurious warning on startup claiming two config directories both contained profiles, naming the same path twice and suggesting you set FORMAE_CONFIG_DIR to disambiguate. The legacy $HOME/.config/formae location and the $XDG_CONFIG_HOME/formae location were in fact the same directory, so there was never a real conflict. The warning no longer appears in this case, and the config directory resolves exactly as before. macOS and WSL were unaffected, since they typically leave XDG_CONFIG_HOME unset.
  • FIXED: The bundled plugin examples were missing from an install. The examples that ship with the bundled plugins are present again under the install’s examples/ directory, and are readable without sudo.
  • FIXED: An auto-reconcile policy showed a change on every apply that never settled, and an inline auto-reconcile policy was saved without its name. Auto-reconcile policies now compare and label correctly.
  • FIXED: On agents backed by an Aurora database, saving an updated resource refreshed only part of its stored record, leaving some details stale. The full record is now saved, matching the SQLite and Postgres backends.
  • FIXED: formae extract gave the generated import stack a description that no longer matched once its resources were adopted. It now reads “Resources imported with formae extract.”

0.87.1

Bug fixes

  • FIXED: Running formae extract on certain resources failed with a Pkl type-constraint error when the resource’s schema included a nested type with a rule relating two optional fields (for example, “exactly one of these two fields may be set”), even when the actual values were valid. These resources now extract correctly.

0.87.0

New features and improvements

  • Manage connection profiles with formae profile. formae now manages named connection profiles (each a complete config for one environment: agent endpoint, targets, credentials) directly from the CLI, instead of hand-swapping config files. Use formae profile list, current, use <name>, save, create, edit, delete, and diff to manage them. The active profile applies to every command; to target a different environment for a single invocation without changing the active selection, pass --profile <name> (mutually exclusive with --config) on any command that connects to the agent. Existing setups migrate automatically the first time you run the CLI, and a fresh install starts with a ready-to-use local profile.
  • Embed references to other resources inside text with formae.embed. A resolvable reference to another resource’s property could previously only be a field’s entire value. You can now splice one (or several) into the middle of a text field with formae.embed("…\(other.res.someProperty)…"): formae resolves each reference at apply time and substitutes the real value into the surrounding text. This lets a value that only exists after another resource is created be used inside a larger literal in a single apply. For example, a CloudFront Function whose JavaScript needs a Key Value Store’s generated Id, functionCode = formae.embed("const kvsId = '\(kvStore.res.id)'; …"), which previously meant applying the store, copying its Id by hand, then applying the function. The embedded reference round-trips through formae extract (it comes back as formae.embed(...), not a frozen value) and is preserved across background synchronization.
  • Force-cancel stuck commands with formae cancel --force. A plain formae cancel stops a command gracefully, waiting for any in-progress resource updates to finish before the command reaches Canceled, so it can wait indefinitely on an operation that never completes (for example a plugin stuck in a poll loop). The new --force flag is an escape hatch: it abandons in-progress work and drives the command to a terminal Canceled state immediately. Because cloud-side operations may still be running, formae asks you to confirm before a forced cancel (pass --yes to skip), and lists any resources whose creation may have been left in flight so you can verify or clean them up. Update and delete operations left in flight are reconciled automatically by background synchronization on its next cycle.
  • Install a production agent on AWS with one command. A new open-source AWS bootstrap installer stands up a complete formae agent (VPC, ECS Fargate task, and database) secure by default, in either of two access modes: a public HTTPS endpoint fronted by an ALB with your own ACM certificate, or a private endpoint reachable only over your Tailscale tailnet with an automatically-provisioned trusted certificate. Basic auth is on in both modes, and a helper writes a ready-to-use connection profile so your CLI points straight at the new agent. This is now the recommended way to install the agent on AWS; the manual ECS walkthrough remains available for hand-tuned deployments. See AWS Bootstrap.

Bug fixes

  • FIXED: A resource property whose value is a list or object taken from another resource’s reference showed a perpetual update on every reconcile: it never settled, even immediately after a successful apply. (A DNS CNAME record whose value is sourced from an ACM certificate’s DNS-validation records is the case that surfaced it.) Such a property now reconciles as a no-op once applied; properties with plain scalar values were never affected.
  • FIXED: When an update caused a dependent resource in a different region or account to be deleted, the delete could fail to locate its target. The resolved target configuration is now carried through to those dependent deletes, so the delete runs against the correct region and account.
  • FIXED: A resource with a list nested inside another list could show a change that never settled (reappearing on every reconcile even straight after a successful apply) when an entry in the inner list drew its value from another resource. An AWS::ECS::TaskDefinition whose container environment variables (a list nested inside the container-definitions list) reference another resource’s output is the case that surfaced it. formae was matching the inner entries using the outer list’s rules, so it paired the wrong entries and kept proposing a phantom update. Inner lists are now matched on their own terms, so these resources reconcile as a no-op once applied.
  • FIXED: Sensitive properties that the cloud provider never reads back, such as passwords and secret tokens, were listed as a change in every apply plan, tagged with a (write-only) label, even when their value had not changed. This cluttered an otherwise clean reconcile and made the plan harder to scan. Such a property now appears in the plan only when its value is actually changing, and the internal label is gone.
  • FIXED: A resource extracted with formae extract and then re-applied showed a perpetual update on any text property whose value contained a double-quote character: the property never settled, and its value gained extra backslashes on each apply. An inline function body (such as a serverless function’s source) is the case that surfaced it. Quotes in extracted text are now escaped exactly once, so these properties round-trip through extract and re-apply as a no-op.
  • FIXED: Deleting a target left its discovered (unmanaged) resources behind in formae’s inventory. Resources that formae had discovered but did not manage were not removed when their target was deleted, so they lingered (pointing at a target that no longer existed) until the next background synchronization cleaned them up, or indefinitely if synchronization was disabled. Deleting a target now removes its discovered resources immediately. Managed resources are unaffected and are still deleted through the normal provider cascade.

0.86.2

Bug fixes

  • FIXED: An opaque value (formae.value(...).opaque) was rewritten whenever another field on the same resource changed, even when the value itself was unchanged. For a rotating opaque value this minted a needless new value every time an unrelated field was edited; for a set-once opaque value (.opaque.setOnce) it could overwrite the stored value with an internal hash. An unchanged opaque value is now left untouched when other fields on the resource change, while a genuine change to the value still applies.
  • FIXED: Updating a list field marked atomic could be rejected by the cloud provider. The change was sent as a per-element remove-and-add pair; for mutually-exclusive lists such as an AWS Network Firewall policy’s default stateful actions, the provider briefly held both the old and the new values and rejected the update. Atomic list fields are now updated as a single wholesale replacement, matching how the provider’s own API applies them.
  • FIXED: When a reference to another resource’s property could not be resolved, the dependent resource failed with an empty error message, before any cloud call, leaving nothing to act on. The failure now names the reference and the missing property (and the source resource where known), so the cause is clear.

0.86.1

Bug fixes

  • FIXED: Background synchronization could permanently stop after certain scheduling overlaps. Synchronization is now resilient to overlapping triggers and continues running until the agent is stopped.
  • FIXED: Auto-reconcile could not converge a stack when an earlier reconcile had failed. If the very first reconcile for a resource failed (for example, a transient outage on one node in a fleet), subsequent auto-reconcile cycles never retried the failed resource. In the worst case, after a partially-failed user reconcile, auto-reconcile would actively revert the resources whose updates had succeeded back to an older state instead of finishing the user’s intent. Auto-reconcile now sources its baseline from the most recent user-submitted reconcile regardless of whether it succeeded in full, so failed updates retry on the next tick and successful ones stay applied.
  • FIXED: Destroying a stack with an auto-reconcile policy could leave the resources in place. The destroy itself completed, but the next auto-reconcile beat treated the pre-destroy resource list as the desired state and recreated everything within seconds, effectively rolling the destroy back. Destroys now contribute to the auto-reconcile baseline alongside applies, so a destroyed stack stays destroyed and the agent stops attempting to reconcile it.
  • FIXED: Slow plugin Read operations could time out before the plugin had a chance to respond. Plugin reads issued during dependency resolution were given a truncated timeout instead of the configured per-operation timeout, causing legitimate slow reads (e.g. cross-region or rate-limited APIs) to fail. Reads now get the full configured timeout.
  • FIXED: Periodic resource discovery could permanently stop after an apply that created a new discoverable target. Discovery is now resilient to overlapping triggers (apply, manual force-discover, and the scheduled timer) and continues running until the agent is stopped.

0.86.0

New features and improvements

Relabeling resources

Extract

  • formae extract handles per-version schema layouts. Plugins that ship schemas split by API version (for example the Kubernetes plugin’s @k8s/v<X.Y>/ subtrees) can now be extracted and re-evaluated without the alias collisions and identifier errors that earlier 0.85.x releases produced when a forma referenced multiple per-version files. This unblocks the Kubernetes plugin 0.1.3 (K8s 1.35 and 1.36 support); see Kubernetes plugin release notes.

PklProject auto-resolve

  • formae auto-resolves PklProject dependencies. Previously, running an apply in a directory with an unresolved PklProject failed with NoSuchFileException and forced a manual pkl project resolve. formae now runs the resolve automatically when PklProject.deps.json is missing.

Cascading updates

  • Mutable parent revisions no longer destroy their dependents. Previously, when a parent resource was replaced, every dependent was cascade-replaced (torn down and recreated) regardless of whether the dependent’s referring field could actually accept the new parent value via a provider-native update. The canonical case is an AWS::ECS::Service consuming an AWS::ECS::TaskDefinition: every routine TaskDefinition revision (an image deploy, an env-var bump) tore the Service down and stood it back up, typically two to four minutes of outage per deploy, even though the Service can absorb a new TaskDefinition revision via a rolling deploy without ever stopping traffic.

Query syntax

  • Same-field OR matching. Repeating a field in a --query now matches any of the listed values. formae inventory resources --query='stack:web stack:api' returns resources in either stack; the exclusion form -stack:scratch -stack:tmp excludes any of them. Cross-field terms continue to AND together as before, so type:AWS::S3::Bucket stack:prod stack:staging still means “S3 buckets in (prod or staging)”.
  • Wildcard matching. Every string-valued field (stack, label, type, target) accepts * as a prefix or suffix wildcard. --query='label:prod-*' matches every resource whose label starts with prod-; label:*-prod matches every label that ends with -prod. Wildcards work across the colons in type names too: --query='type:AWS::S3::*' matches every S3 resource type. Only * is supported (no ?), and a bare * is rejected: wildcards must be anchored to at least one literal character.
  • target: filter on resource queries. Resource queries now accept target:<label> to scope results to a specific cloud target, matching the behaviour that already existed for stack, type, and label. Previously this filter was silently ignored.

Datastore

  • Microsoft SQL Server datastore backend. The agent can now persist its state to Microsoft SQL Server, including Azure SQL Database, alongside the existing SQLite, PostgreSQL, and Aurora Data API options. Set datastoreType = "mssql" in the agent configuration; this is the natural choice for Azure deployments. See Configuration → Datastore for the connection settings.

CLI

  • Replace simulations show the full diff. A formae apply simulation of a replace operation now surfaces any non-immutable changes (mutable property edits, label rename) in a follow-up and by doing the following: block alongside the existing because these immutable properties changed: block. Operators see the full picture of what an apply will do, not just the immutable trigger.

Documentation

  • Published roadmap. The docs now carry a rolling, three-release roadmap so you can see where formae is heading over the next few releases, alongside the release notes that record what has already shipped.

Bug fixes

  • FIXED: Stack apply and destroy could get stuck on certain resource ordering dependencies. Some cloud resources hold their parent alive through a network attachment or rule that the provider requires you to remove first: an EFS file system’s mount targets hold the file system open through an ENI; a security group’s rules hold live workloads on the wire. formae treated these as ordinary parent-child references and could try to delete the parent in parallel with the dependents, leaving orphaned resources behind and the apply command itself wedged waiting for the provider to confirm a delete it was never going to confirm. The canonical case is a stack with an AWS::EFS::FileSystem and an AWS::ECS::TaskDefinition that mounts it. Before this release the destroy looked like:
    The same shape bites on create. The TaskDefinition would start before its MountTargets were operational, the ECS task would fail to mount EFS, and the apply would burn retry cycles until the mount targets caught up. Plugins can now declare which fields create ordering dependencies, and formae uses that information to order the destroy and create graphs correctly. With the AWS plugin ≥ 0.1.11:
    This also fixes the symmetric destroy case for SecurityGroupIngress / Egress rules, previously torn down in the first destroy wave, now sequenced after the workloads they protect. See AWS plugin 0.1.11 release notes for the resource-type-specific list.
  • FIXED: A failed Read during a background sync no longer leaves the agent unresponsive to subsequent apply, destroy, or sync commands until restart.
  • FIXED: Optional collection fields (a Tags list, a metadata map) absent in the forma no longer produce spurious “remove” patch ops when the provider returned [] or {}, which previously caused some resource types (e.g. AWS::ECS::TaskDefinition.Tags) to fail apply with “Member must have length greater than or equal to 1” errors.
  • FIXED: Resources deleted and re-created out-of-band with the same provider identifier (common for some Azure resource types) are now re-discovered.
  • FIXED: Plugin error messages now surface on Update, Delete, and resume failures instead of being dropped in favour of a bare UnforeseenError.
  • FIXED: formae update list refreshes package metadata before listing, so the output reflects what’s actually available on the Hub instead of stale local cache.
  • FIXED: Intermittent 5-second “timed out” errors at the start of plugin-side resource operations are resolved.

0.85.0

Breaking changes

  • Plugin distribution moves to the public platform.engineering Hub. Plugins are no longer bundled with the formae binary. They are installed on demand from the community orbital repo on hub.platform.engineering via formae plugin install <name>. The container image and setup.sh have been updated to install the curated default set (formae-plugin-standard) automatically, so a fresh install behaves the same as 0.84. Existing 0.84 installs that relied on bundled plugins must install the plugins they were using after upgrading. See the upgrade notes below.
  • artifacts configuration block reshaped. The new artifacts.repositories listing replaces the flat artifacts.url / artifacts.username / artifacts.password fields. Defaults already include both the pel (binary) and community (formae-plugin) repos, so users on the default config don’t need to change anything. Users who set any of the deprecated fields will see deprecation warnings on agent startup. See Upgrading from 0.84 for the migration snippet, and Configuration → Artifact repositories for the new shape.

New features and improvements

The Public Hub

The platform.engineering Hub at hub.platform.engineering is now the single source of truth for distributing the formae binary, plugins, and tooling. It hosts:
  • The pel repo: the existing binary repository, where the formae binary, pkl, and other system tooling live. This is what setup.sh and formae update already pull from.
  • The community repo: the new repository for formae plugins. Resolves formae plugin install <name> requests, signed by the publisher and verified against the platform.engineering root CA on every install.
Both repos are reachable on every supported platform (linux-x86_64, linux-arm64, macos-x86_64, macos-arm64). The trust chain ties every published artifact back to a publisher cert chained to a platform.engineering root, so install-time verification works offline once the metadata cache is warm.

Plugins

  • formae plugin install / uninstall / update / search / info / list commands. Plugin lifecycle is now fully managed through the CLI. install and update accept optional @version pins. search filters by category, type, and channel. In 0.85, install, uninstall, and update operate locally. Run them on the agent host (typically sudo formae plugin install <name>) for resource plugins; run them on every host that runs the CLI or the agent for auth plugins. formae plugin list shows the agent and CLI views together with version-mismatch warnings. Container deployments are the natural single-step case: bake the install into the Dockerfile and the running container stays immutable. See formae plugin for the full reference.
  • The agent runs unprivileged. As part of the move to the new plugin distribution, /opt/pel is owned by root and the formae agent process runs as the pel user. formae plugin install and friends re-exec themselves under sudo to write into /opt/pel. A future release will restore the single-command “install on every host” UX for auth plugins.
  • Standard plugin metapackage. The new formae-plugin-standard metapackage pulls in the curated default plugin set: aws, azure, gcp, oci, ovh, and auth-basic. Container builds and the install script use it so a fresh install ships with the same plugins as 0.84 had bundled.
  • Centralized plugin builds. Every platform.engineering plugin is now built and signed in a single, locked-down workflow (formae-actions/plugin-build.yml). Plugin repos no longer hold signing material: they only carry a thin dispatch wrapper. Tag-only refs, single-SHA pinning, and a static repo allowlist guard the pipeline. The publisher signs every artifact with an intermediate cert chained to the platform.engineering root CA, and the community repo verifies that chain on install.

CLI

  • formae extract reuses your existing PklProject. When the target directory already has a PklProject, extract reads its declared dep versions and uses them for the generated .pkl instead of the latest installed schemas. The extracted output evaluates cleanly against your existing project, with no version skew between extraction and apply. When extracting into a fresh directory (no PklProject), the existing behavior is preserved: a new PklProject is generated using the locally installed plugin versions.
  • formae project init --plugin-dir. New flag on project init for plugin developers iterating on @local schemas. Defaults to ~/.pel/formae/plugins so the typical workflow keeps working.

Kubernetes plugin

  • New plugin. Typed Pkl schemas per K8s minor (v1.21 through v1.34): field mismatches fail at pkl eval time, not against your live cluster. Six Auth classes: KubeconfigAuth, InClusterAuth, EKSAuth, AKSAuth, GKEAuth, OCIAuth. See Kubernetes plugin.

Helm integration

  • Helm integration. Deploy upstream Helm charts as part of your forma. Reference a chart by name and version, set values inline.

.tfvar integration

  • .tfvars files can now be used directly in formae

Upgrading from 0.84

The 0.84 → 0.85 update preserves bundled plugins. Inside the 0.85 install, ops install formae will pull in the standard metapackage on first start, so you’ll have the same plugin set you had before. If you need a non-standard plugin (compose, grafana, sftp), install it explicitly:
After upgrading, you can verify what’s installed with:
If your 0.84 install was running a version of auth-basic, that plugin is already present on both the agent and the CLI hosts. In 0.85 the upgrade is local-only, so run sudo formae plugin upgrade auth-basic on each host where the plugin lives (one command on a single-host deployment, one per host on a split deployment).

Artifact configuration

If your formae.conf.pkl does not override artifacts, no action is needed: the default config now includes both the pel (binary) and community (formae-plugin) repositories. Plugin install just works. If your formae.conf.pkl set any of artifacts.url, artifacts.username, or artifacts.password, you’ll see deprecation warnings on agent startup. Migrate to artifacts.repositories:
The username and password fields are deprecated without a like-for-like replacement in 0.85; per-repository credentials will be reintroduced in a later release. Operators relying on basic auth against the hub should reach out before upgrading.

0.84.0

Breaking changes

IMPORTANT: Manual installation required for this release. formae now uses the orbital package manager for installation and updates, so the formae upgrade command from previous versions cannot upgrade to 0.84.0. Install 0.84.0 using the one-line installer:
After this one-time installation, future updates use formae update. See Installation for details.
  • upgrade command replaced by update: The formae upgrade command has been removed. Use formae update instead. The new command uses the orbital package manager and supports version pinning and channel selection. See update for details.
  • MessagePack wire protocol (plugin developers): The internal communication protocol between the agent and plugins has been replaced from Ergo’s native EDF struct encoding to MessagePack with zstd compression. This is a foundational change that enables backwards-compatible schema evolution: future SDK releases can add new fields without requiring all plugins to be recompiled simultaneously. All existing plugins must be recompiled against Plugin SDK v0.2.1 for this release; plugins built against older SDK versions are detected as incompatible and skipped at startup with a clear warning message. See Plugin SDK v0.2.1 for migration details.

New features and improvements

Configuration

  • Per-plugin configuration: Resource plugin behavior can now be tuned per plugin instead of globally. In the new agent.resourcePlugins block you can enable or disable individual plugins, set per-plugin rate limits, declare which resource types each plugin discovers, override retry behavior, and customize how discovered resources are labeled. Plugins can also ship their own typed configuration schema (schema/Config.pkl) to expose plugin-specific settings. The global agent.retry, agent.discovery.resourceTypesToDiscover, and agent.discovery.labelTagKeys settings are deprecated in favor of these per-plugin overrides. See Configuration for the full reference.
  • Configuration blocks relocated: Authentication and network settings move from the plugins { ... } block to purpose-specific locations. Agent-side auth goes in agent.auth, CLI-side auth in cli.auth, and network config at top-level network. The previous plugins { authentication { ... } } and plugins { network { ... } } blocks are still supported with deprecation warnings. See Configuration and Security and networking.

Targets

  • Per-field config mutability: Target config fields can now be annotated as mutable or immutable by plugin authors. Changing a mutable field (like an AWS profile) produces an in-place target update without recreating resources. Changing an immutable field (like a region) triggers a full target replace as before. This is backwards compatible: targets without annotations treat all config changes as immutable. See Per-field config mutability for details.
  • Config change visibility: The simulate output now shows which target config fields changed and how (added, changed, removed), consistent with how resource property changes are displayed.

Resolvables

  • Reference individual items in collections: You can now pick a specific value out of a collection property instead of getting the entire collection. Use .at("key") for maps and .at(index) for lists. For example, reference a specific compose stack endpoint directly in your Grafana target:
    This replaces the previous Endpoints/EndpointKey workaround. See Collection Resolvables.

Plugins

  • Datadog plugin: New official plugin for managing Datadog resources: monitors, SLOs, downtimes, log indexes, pipelines, archives, dashboards, teams, roles, and synthetics tests. See the Datadog plugin documentation.
  • GitHub Actions plugin: New official plugin for managing GitHub Actions resources: secrets, variables, environments, branch policies, workflow files, OIDC claims, runner groups, and org-level permissions. See the GitHub Actions plugin documentation.
  • GitLab plugin: New official plugin for managing GitLab project-scoped CI/CD resources: variables, environments, repository files, and typed .gitlab-ci.yml pipelines. See the GitLab plugin documentation.
  • Plugin compatibility checks: The agent now verifies plugin compatibility at startup and skips incompatible plugins with clear, actionable warnings instead of crashing. Checks work in both directions: plugins requiring a newer agent and plugins built against an older SDK are both detected. Plugins also verify agent compatibility at startup as a fallback.

Bug fixes

  • FIXED: Destroying a target with discovered (unmanaged) resources left those resources orphaned in the database, causing the sync cycle to crash or produce repeated errors. Discovered resources are now unregistered correctly when their target is destroyed, and a related registration leak that could permanently block discovered resources from future sync cycles is resolved.
  • FIXED: Patch-mode apply could produce spurious diffs when a resource contained nested objects whose children had all been stripped as provider defaults (e.g. Lambda DestinationConfig.OnSuccess/OnFailure). The now-empty parent object was left in the desired state, causing CloudControl to reject the update with “required fields missing”.
  • FIXED: Patch-mode applies could show spurious diffs on resources with fields that are set at creation and never returned by the provider afterwards (typically secrets, tokens, or credentials). Repeated applies are now idempotent for these resources: the unreadable field is excluded from comparison instead of appearing as a phantom change.
  • FIXED: Non-portable target replace errors now return HTTP 409 (Conflict) with a clear message instead of HTTP 500 with a raw error string.
  • FIXED: Target create errors during crash recovery are logged at debug level instead of error, since the idempotent retry handles them gracefully.
  • FIXED: CLI help output previously generated documentation deep links that returned 404s. Help text now links to the docs root.
  • FIXED: Reduced log noise from the underlying state machine framework by demoting internal timeout messages from info to debug level.
  • FIXED: Default values filled in by the cloud provider on nested resource properties (for example fields AWS sets on ECS container definitions when you leave them unset) weren’t recognised as such. On repeated applies of an unchanged forma, the agent saw these as real changes and could choose to recreate the resource instead of treating the apply as a no-op. Nested provider defaults are now ignored during comparison, so reapplies are idempotent.
  • FIXED: Targets whose config references resolvables no longer report false changes on repeated applies. Previously, applying the same forma twice in a row would flag the target as modified because the stored config carried the resolved value alongside the reference. Same forma → same apply outcome.
  • FIXED: Evaluating formae PKL files no longer fails with confusing Cannot find property allAnnotations errors when an older pkl binary happens to be first on your PATH. The CLI now uses the pkl binary shipped alongside the formae executable, so you don’t need to manage your PATH manually.
  • FIXED: Simulate output now renders composite values in remove operations as JSON instead of Go’s map[k:v] format, making add/remove diffs visually comparable.
  • FIXED: PKL schema hints for fields nested three or more levels deep inside sub-resources were silently dropped when the intermediate sub-resource had no @FieldHint annotation of its own. This could cause provider defaults deep in the schema to not be stripped, leading to spurious replacements on reapply.
  • FIXED: Resources with nested lists whose element order differed between stored state and desired state (for example, ECS Environment variables or PortMappings) could trigger spurious replacements. List comparison now ignores element order recursively.
  • FIXED: formae extract failed on resources with Float properties (for example, GKE Container Cluster’s batchPercentage). These resources can now be extracted correctly.

0.83.2

Bug fixes

  • FIXED: Docker image: the .config directory was created as root during the plugin migration build step, preventing the pel user from writing to it at runtime. It is now chowned to pel alongside .pel.

0.83.1

Bug fixes

  • FIXED: Apply and destroy commands could get permanently stuck at InProgress after an agent restart. Target update states (e.g. target creation completing successfully) were only held in the agent’s in-memory cache and never written to the database. If the agent restarted before the command completed, the target state reverted to NotStarted, causing overallCommandState to return InProgress indefinitely.
  • FIXED: A single failed discovery sync read (e.g. an AWS resource that CloudControl cannot read) could cascade-fail all other reads in the same sync command. Discovery sync commands were incorrectly creating dependency edges between independent read operations, so one failure would propagate to unrelated resources.

0.83.0

New features and improvements

Plugins

  • Grafana plugin: New official plugin for managing Grafana resources: dashboards, data sources, folders, alerting rules, teams, and service accounts. Works with both self-hosted Grafana and Grafana Cloud. See the Grafana plugin documentation.
  • Docker Compose plugin: New official plugin for managing Docker Compose stacks as infrastructure. Exposes resolvable endpoints that other plugins can reference, for example a Grafana target can resolve its connection URL from a compose stack. See the Docker Compose plugin documentation.

Targets

  • Target replace: When a target’s configuration changes (e.g. switching AWS region), formae now replaces the target and all managed resources in the correct order: delete resources, delete target, create target, recreate resources. This is only allowed when all resources on the target are marked as portable in their schema, ensuring safe recreation. In reconcile mode, stacks not present in the forma are preserved; in patch mode, everything managed on the target is replaced.
  • Resolvable target configuration: Target configuration fields now support resolvable references to resource properties. This enables cross-provider workflows where one provider’s output feeds another provider’s configuration, for example a Kubernetes target whose cluster endpoint is resolved from an EKS resource. Targets with resolvable config are automatically ordered in the execution DAG to wait for their dependencies. See Target resolvables for examples.
  • Cascade target deletes: When destroying resources that a target depends on (via resolvable references), formae detects the dependency and either aborts (default) or cascade-deletes the dependent target and its resources. Use --on-dependents=cascade to proceed. Plain targets (e.g., cloud region targets) without resolvable dependencies survive destroy.

Operations

  • Azure cloud installer: One-command deployment of the formae agent to Azure Container Instances with managed identity authentication. See Install on Azure.
  • GCP cloud installer: One-command deployment of the formae agent to Google Cloud Run with Workload Identity Federation. See Install on GCP.

Schema

  • Atomic field updates: Fields annotated with updateMethod = "Atomic" are now compared as opaque values and produce a single replace operation instead of recursive sub-field diffs. This is needed for cloud provider fields like IAM policy documents where the API expects the entire value as a single update. See Collection Semantics for details.

Bug fixes

  • FIXED: Synchronization and updates could silently fail for resources with complex schemas (e.g. Kubernetes Pod, Deployment, AWS ECS TaskDefinition). Large messages between the agent and plugins were being dropped due to an internal transport limit. All plugin messages are now compressed, eliminating this size restriction.
  • FIXED: Agent could fail to recover after a crash, leaving commands stuck in an incomplete state.
  • FIXED: Concurrent apply commands targeting the same resource could create duplicates.
  • FIXED: Commands could reach terminal state before resource properties were fully persisted, causing stale inventory data.
  • FIXED: Concurrent stack deletion could cause panics.
  • FIXED: Recreating a resource after deletion could produce stale internal references.
  • FIXED: A background sync could interfere with an in-progress apply, causing unexpected failures.
  • FIXED: An update on a resource with resolvable references could get stuck if resolving the reference failed.
  • FIXED: formae apply could generate unnecessary resource replacements when createOnly fields hadn’t actually changed.
  • FIXED: formae apply could generate spurious patch operations from empty arrays and maps produced by the PKL schema, leading to unnecessary replacements or validation errors.
  • FIXED: Provider-populated default values inside nested sub-resources (e.g. container CPU defaults in ECS TaskDefinitions) were incorrectly flagged as drift.
  • FIXED: Provider-populated default values in key-based collections (e.g. AWS LoadBalancer attributes) could cause patches to exceed API limits.
  • FIXED: Resources with required fields inside array properties (e.g. ClusterRole rules) could be silently rejected during validation.
  • FIXED: References nested inside array elements were not resolved during patch generation.
  • FIXED: formae extract produced invalid PKL in some cases involving stack references.
  • FIXED: Discovery could skip resources between cycles.
  • FIXED: Plugin error messages were not included in failure responses, making debugging difficult from the CLI.

Breaking changes

Plugin SDK update required. This release changes the internal message format between the formae agent and plugins. All plugins must be recompiled against pkg/plugin v0.1.21 to remain compatible. Plugins built against older SDK versions will fail with message decoding errors during synchronization and resource operations.

0.82.3

New features and improvements

  • Pkl extraction: Shared types are now imported during formae extract resolving issues where extracted Pkl code referenced types that weren’t in scope.

Bug fixes

  • FIXED: Container image did not include cloud provider plugin binaries due to a missing dependency in the build process. All resource plugins (AWS, Azure, GCP, OCI, OVH) are now correctly packaged.
  • FIXED: formae apply in certain cases incorrectly rejected changes when modifications were already absorbed in the forma (e.g. via extract).
  • FIXED: Empty arrays [] and maps {} were incorrectly treated as missing values during patch comparison. This caused commands that remove all remaining items from a collection to silently leave items behind, and prevented drift detection from catching out-of-band additions to fields not present in the user’s Pkl.
  • FIXED: CommandStatus API endpoint was not reading the command ID from the path parameter.
  • FIXED: API returned incorrect HTTP status codes for no-op and simulate commands.
  • FIXED: Agent could fail to start after a forced termination (SIGKILL, crash, or system reboot) because the stale PID file was not cleaned up. The agent now verifies the recorded PID is a live process and removes stale or corrupt PID files automatically.
  • FIXED: Resolvables referencing nonexistent resources were silently skipped during translation, only surfacing at runtime. Simulate would show a clean plan while the actual apply would fail. These are now caught early and reported as a clear error.
  • FIXED: formae extract accepted a directory path as target, failing later with a confusing error. It now validates the target path upfront and returns a clear message.

0.82.2

Bug fixes

  • FIXED: Simulation output now shows the actual value being removed from arrays instead of displaying “(empty)”.
  • FIXED: Several Pkl union types with nullable alternatives were incorrectly declared.

Documentation

  • Installation guide restructured: Installation documentation is now split into dedicated pages for local, Docker, and Helm deployments.
  • Collection handling: New section in Apply Modes explaining how reconcile and patch modes differ when updating collections.

Plugin release notes

Official plugin release notes have moved to dedicated pages under each plugin’s documentation. See AWS, Azure, GCP, OCI, and OVH.

0.82.1

Bug fixes

  • FIXED: Running formae extract on discovered resources could crash when all results were unmanaged or when results included a mix of managed and unmanaged resources. The $unmanaged stack doesn’t exist in the database, so it was silently dropped during serialization, leaving the PKL generator with missing stack data. Both cases are now handled correctly.
  • FIXED: Conformance tests could fail for resources that have Resolvable references nested inside SubResource maps. The test runner was comparing these maps as flat strings instead of recognizing the nested references. The framework now recursively walks maps and correctly compares Resolvable fields at any depth.

0.82.0

New features and improvements

  • Policies (Beta): formae now supports policies: configurable behaviors that you attach to stacks, that allow you to choose different automation strategies for different sets of resources and use cases. This release introduces TTL policies for automatic cleanup after a duration, and auto-reconcile policies for automatic reversal of out-of-band and incremental changes at regular intervals. Policies can be inline (owned by a stack) or reusable (shared across stacks via .res reference). See the Policy documentation for details.
  • AI assistant integration (Beta): formae now ships with an MCP server and 13 skills that teach AI coding assistants how to manage infrastructure and open the way to partly or fully agentic, safe, IaC-centric workflows. Deploy, query, detect drift in your own codebase, and build plugins, all through natural conversation. Available as a Claude Code plugin via the formae marketplace. See the AI coding assistants documentation for details.
  • New formae inventory policies command to list all reusable policies. See the CLI documentation for details.
  • The formae inventory stacks command now shows attached policies for each stack.
  • HasProviderDefault field hint: New hasProviderDefault annotation for fields where cloud providers assign default values. When a field with this hint exists in actual state but is not specified by the user, formae accepts the provider’s default instead of generating a “remove” operation. This prevents oscillation during reconcile for fields like S3 bucket encryption or SQS visibility timeout. See the schema reference for details.
  • How-to guides: documentation now includes a growing number of concrete guides how to use formae in different situations and for various use cases.

0.81.0

New features and improvements

  • Stack improvements: Stack descriptions are now correctly stored and updated when reapplying. Stacks are automatically deleted when their last resource is removed.
  • New formae inventory stacks command to query and list all stacks managed by formae. See the CLI documentation for details.
  • Aurora Data API datastore: New datastore option for AWS deployments using AWS RDS Data API. See the configuration documentation for details.
  • Empty targets can now be deleted using formae destroy. See the target documentation for details.
  • Non-interactive plugin init: The formae plugin init command now supports non-interactive mode for automation, CI pipelines, and LLM-assisted workflows. Use the --no-input flag with CLI flags to provide all required values. See the plugin tutorial for details.
  • Plugin dashboard: A new Grafana dashboard for monitoring plugin metrics is available in the formae-grafana-dashboards repository.
  • Plugin log messages now show the plugin name instead of the namespace.

Bug fixes

  • FIXED: Resources with fields that are arrays of resolvables are now handled properly.
  • FIXED: Plugin shutdown no longer produces spurious error messages.
  • FIXED: Improved import declaration generation when using formae extract.

0.80.1

Bug fixes

  • FIXED: The extract command would fail for plugins where the plugin name differed from the namespace.

0.80.0

Breaking Changes

  • Plugin SDK: Complete overhaul of the ResourcePlugin interface, the formae Pkl schema and plugin distribution. See the plugin SDK documentation for details.
  • Formae Pkl schema: nonprovisionable and persist resource annotations have been removed. Tags are no longer a first-class citizen in formae and will be superseded by plugin-specific implementations. For AWS, formae.Tag will be supported for three more releases before being removed. Please update tag definitions in your AWS Pkl files to use aws.Tag instead of formae.Tag.

New features and improvements


0.76.5

Bug fixes

  • FIXED: In some cases updates for AWS Tags were generated incorrectly, causing commands to fail.
  • FIXED: If there were unfinished commands on agent startup, the agent would some times try to resume those commands before the plugins were fully initialized.
  • FIXED: Commands would fail when a read operation failed with a recoverable error (like a rate limit).
  • FIXED: AWS::EC2::TaskSet was incorrectly marked as discoverable causing noise in the logs during discovery.

0.76.0

Breaking Changes

  • Plugin Architecture Migration: Plugins must now be built as standalone executables instead of shared libraries (.so files). Existing custom plugins need to be updated with a main() function that calls plugin.Run(). See the AWS plugin implementation for reference.

New features and improvements

  • Plugin Architecture Overhaul: Plugins now run as separate executables in their own processes rather than as shared libraries, enabling independent dependency graphs and multi-host deployment.
  • Major Performance Improvements: Significantly improved command processing performance. Previously limited to around 500 resources, the agent now handles up to 20,000 resources. See the agent sizing recommendations for resource requirements.
  • Enhanced Observability: The agent now exports comprehensive telemetry via OpenTelemetry including metrics, structured logs with trace correlation, and distributed traces for API requests, resource operations, and database queries.
  • Grafana Dashboards: Pre-built Grafana dashboards for monitoring formae are now available in the new formae-grafana-dashboards repository.
  • New temporality configuration option for OTLP metrics export, supporting both delta (OTel-native) and cumulative (Prometheus/Mimir) temporality.

Bug fixes


0.75.5

New features and improvements

  • Shell auto-completion is now supported for formae commands in bash and zsh. The installer can now install the necessary completion scripts
  • Sync commands that did not pull in any changes are now automatically removed from the database

Bug fixes


0.75.4

Bug fixes


0.75.3

Bug fixes


0.75.2

New features and improvements

  • Watch output prints useful hints at the end of the output
  • Targets are now discoverable by default
  • When creating a target, discovery is automatically initiated
  • Discovery now traverses multuple levels of parent-child relationships in resource hierarchies
  • Discovery can now handle multiple namespaces
  • Discovery creates resolvables to parent properties in child resources
  • Destroy queries no longer accept “managed” as a field parameter, as it does not apply to destroy operations

Bug fixes


0.75.1

New features and improvements

  • Implemented database migration for future changes
  • Discovery is enabled now by default in the configuration
  • Targets are now discoverable by default
  • Default discovery interval is now at 10 minutes instead of previously 1 hour

Bug fixes


0.75.0

New features and improvements

  • New formae cancel command that allows to cancel any apply or destroy command already being executed by the agent.
  • New formae inventory targets command to query and list targets with filtering by label, namespace, and discoverability.
  • Targets now support a discoverable field for granular control over which targets participate in discovery scans.
  • Pkl: Nested fields are now being parsed.
  • The lifeline example now uses more idiomatic PKL code.

Breaking changes

  • We are deprecating configuration of discovery targets through the agent config. Instead, targets need to be configured through forma application now.

Bug fixes


0.74.0

Public launch.