formae apply you run from a laptop
works in a CI/CD job once you turn off the interactive prompts and read
structured output instead of tables. This guide covers the non-interactive
contract, the simulate-in-a-pull-request then apply-on-merge flow, passing
values between stages, worked GitHub Actions and GitLab CI pipelines, reading
discovered resources, and fanning the same pipeline out across many
repositories.
The Git as the source of truth operating
model is the natural fit here: your forma files live in Git, the pipeline is the
only thing that applies them, and every change lands through a reviewed merge.
Run formae non-interactively
A pipeline runner needs the formae CLI and a way to reach the agent. The agent is long-lived and shared: you do not start one per job. Many pipelines and humans connect to the same agent at once. Install the CLI in the runner the same way you would locally:The runner has to reach the agent. The
default
profile points at
http://localhost:49684, which is only right when the agent runs on the same
host. For a remote agent, add a profile whose cli.api.url is the agent host and
select it per command with --profile, or point the CLI at the agent through
your CI secrets.apply into something a pipeline can drive:
Gate the pipeline on the outcome
apply submits the command to the agent and the agent executes it
asynchronously. In machine mode, apply prints the command ID as soon as the
command is accepted:
formae status command until the command reaches a terminal state:
apply is fire-and-forget there: it
submits the command and returns as soon as the agent accepts it. Use the
formae status command poll above wherever the pipeline result has to gate a
merge or a deploy - it streams status and fails the job when the apply fails.
Simulate in the pull request, apply on merge
The everyday pipeline has two triggers. On a pull request, validate the forma and preview the changes without touching anything. On merge to the main branch, apply them.1
Validate in the pull request
formae eval catches syntax and type errors. Running apply with
--simulate previews the plan without making any cloud API calls, so
reviewers see exactly what the merge would do.2
Apply on merge
Once the change lands on the main branch, apply it. Reconcile mode brings the
stack into the exact shape the forma describes.
Parameterize per environment
You rarely want a separate forma per environment. Declare properties for the parts that vary and formae turns each one into a CLI flag automatically. One forma then deploys to staging or production by passing different flags.--env exists because the forma declares an env property. To see which
flags a forma exposes, pass it to --help:
Properties: section with each flag, its default, and whether
it is required. For the full pattern, including how to constrain inputs so a bad
value fails before any cloud call, see
Build self-service infrastructure.
Pass values between stages
A common shape is provision, then deploy, then verify: one job creates infrastructure, the next needs a computed value from it (a hostname, an ARN, a generated name), and the last smoke-tests the result. The machine output offormae inventory is the bridge between stages.
Computed values live under ReadOnlyProperties; values you declared in the forma
live under Properties. Query the resource you just created and pull the field
you need:
$GITHUB_OUTPUT and reads it back through needs.provision.outputs. GitLab CI
writes it to a dotenv artifact and later jobs that needs this one get the
variable injected automatically. The formae side of the bridge is identical: one
formae inventory query, one jq extraction.
Read a discovered resource in a pipeline
Sometimes a stage needs a value from a resource that lives in your cloud account but is not managed by formae: a legacy database owned by another team, shared infrastructure, anything that predates formae. Hard-coding the value in a CI variable works once and then rots. Reading it through discovery keeps the pipeline correct for as long as the resource exists. First make the resource discoverable by applying a target that points at the account or region where it lives. No stack, no resources, just a target:managed: false: visible to inventory and extract, untouched by
apply. Once the resource has been picked up, query it exactly as you would a
managed one, with a managed:false filter:
orders-db.pkl to add a stack, then
formae apply --mode reconcile orders-db.pkl. For a larger estate, slice by
team, type, or environment, one stack per slice. See
Bring resources under management
for the full adoption path.
Provision the pipeline itself as a forma
The pipelines above run formae. You can also let formae manage the pipeline. When you have a plugin for your CI system, the workflow file, the variables and secrets it depends on, and the environments it deploys into are all resources. Oneformae apply lays them all down.
.github/workflows/deploy.yml into the repository as a
managed resource. When the workflow definition is code, editing it is an ordinary
forma change that flows through the same review-and-apply pipeline as everything
else.
Fan out across many repositories
To run the same pipeline shape across many repositories with per-repository overrides, wrap the pieces in a Pkl class and instantiate it once per service. Each instance carries its own target, so a single forma provisions the whole fleet.service-fleet stack that is no longer declared gets
removed, so the forma stays the single source of truth for the whole fleet. The
class pattern is covered in depth in
Reuse with modules.
Tips and gotchas
One agent serves many clients. Multiple pipelines and humans can connect to the
same agent at the same time. Do not start an agent per job.
See also
- Apply modes: reconcile versus patch, and what each one is allowed to do.
- Ways to work with formae: where a Git-driven pipeline sits among the operating models.
- Build self-service infrastructure: turn properties into a parameterized interface developers drive with flags.
- Discovery: how formae finds and tracks resources you have not brought under management yet.
- CLI reference: every command and flag in full.

