The Plan → Apply Flow
- A plan run reads your current state and code, then produces a plan showing what would change
- The plan is stored as a binary file and parsed for resource-level changes
- Governance evaluates the plan (policies, approval rules, external checks)
- If governance passes, an apply run executes the stored plan against your infrastructure
- On successful apply, the changeset is marked as
applied_at
Creating a Plan
POST /workspaces/{id}/runs
This creates a new changeset (with source_type: "api") and a queued plan run.
Applying a Plan
POST /workspaces/{workspaceId}/runs/{runId}/apply
Creates an apply run from a completed plan run.
Requirements
- The plan run must be in
completedstatus (409 otherwise) - The run must be a plan type (400 otherwise)
- The changeset must not be discarded (409 otherwise)
- For Terraform/OpenTofu: the plan must have at least one change (409 otherwise — no-op plans cannot be applied)
- For PR-triggered plans: the workspace must have
allow_pr_apply: true(403 otherwise)
Discarding a Changeset
POST /workspaces/{workspaceId}/changesets/{changesetId}/discard
Marks a changeset as discarded, preventing any future applies. Returns 409 if the changeset is already applied or discarded.
Changeset Sources
| Source Type | How it’s created |
|---|---|
git | Push or PR webhook triggered a plan |
api | Manual POST /workspaces/{id}/runs call |
Idempotency Guard
Forgecroft uses an atomic CTE (Common Table Expression) to guard against concurrent discard and duplicate applies. If two users try to apply the same plan simultaneously, only one succeeds.
Drift Detection on Apply
When an apply run executes, Forgecroft verifies the plan matches the approved plan counts. If the plan has drifted (different resource counts), the apply returns 409. This prevents applying a plan that differs from what was reviewed.
Related API Endpoints
POST /workspaces/{id}/runs— Create a plan runPOST /workspaces/{workspaceId}/runs/{runId}/apply— Apply a completed planPOST /workspaces/{workspaceId}/changesets/{changesetId}/discard— Discard a changesetGET /workspaces/{id}/changesets— List changesets for a workspace