Skip to content

Defining a workflow

Defining a workflow

A custom workflow replaces the default workflow entirely for a project. You write it as JSON in project.workflow_definition, and Forge validates and caches the resolved definition.

This page covers the JSON shape, the validation rules, and how to apply workflows safely to projects that already have tasks. For the conceptual model, see How workflows work.

Minimum viable workflow

The smallest valid workflow has one initial state, at least one terminal state, and every other state reachable from the initial state.

{
"roles": [
{ "name": "coder", "label": "Coder" }
],
"states": [
{
"name": "ready",
"kind": "initial",
"column": "Todo",
"label": "Ready"
},
{
"name": "coding",
"kind": "active",
"role": "coder",
"column": "In Progress",
"label": "Coding"
},
{
"name": "done",
"kind": "terminal",
"column": "Done",
"label": "Done"
},
{
"name": "cancelled",
"kind": "terminal",
"column": "Done",
"label": "Cancelled"
}
]
}

Apply it by PATCHing the project:

Terminal window
curl -X PATCH :8080/api/v1/projects/$PROJECT_ID \
-H 'authorization: Bearer fg_…' \
-H 'content-type: application/json' \
-d '{"workflow_definition": "<JSON string>"}'

When workflow_definition is empty or "{}", Forge resolves to the built-in default workflow at runtime.

Field reference

roles

A list of declared roles. Each role can be referenced by state.role.

FieldTypeNotes
namestringRole identifier. assignee is reserved by the engine.
labelstringDisplay label for the UI.

states

A list of states. The order matters for column primary-state resolution (the first state declared for a column is the drag target).

FieldTypeNotes
namestringState identifier, unique within the workflow.
kindenumOne of backlog, initial, active, gate, terminal, custom.
rolestring?Optional role binding. Active states default to assignee when omitted.
columnstringKanban column label. Multiple states can share a column.
labelstringDisplay label for the UI.
transitionslist?Outbound transitions. Optional; the cancellation path is implicit.
gate_configobject?Only for kind = gate. See below.
hooksobject?Per-phase hooks: before_exit, on_exit, on_enter, after_enter, before_enter. See Lifecycle hooks.

state.transitions[]

FieldTypeNotes
tostringTarget state name.
labelstringDisplay label for the edge.
audienceenumall, agent_only, user_only — who can trigger it.

state.gate_config

FieldTypeNotes
max_rejectionsintRetry budget. See Gates and retries.
reject_targetstringWhere the task lands on rejection (typically the upstream active state).
requires_human_approvalboolWhen true, the gate’s auto-approve path is disabled.

Top-level fields

FieldTypeNotes
cancellation_statestring?Implicit cancellation target. Defaults to a terminal cancelled state if present.
initial_statestring?Override the resolved initial state if you have an unusual graph. Most workflows omit this.

Validation rules

The API rejects a workflow definition with HTTP 400 if any of these fail:

  • Exactly one state has kind: initial.
  • At least one state has kind: terminal.
  • Every non-initial state is reachable from the initial state.
  • No state references a role that isn’t declared in roles.
  • state.role = "assignee" is only allowed on kind: active states.
  • Every gate_config.reject_target references an existing state.
  • column values are non-empty strings.

If validation fails, the response body includes the specific rule that fired so you can fix the JSON and retry.

Updating a workflow safely

Forge protects projects with active work from breaking workflow changes. When you PATCH workflow_definition:

  • States that have no active tasks can be added, renamed, or removed freely.
  • States that do have active tasks cannot be removed. The API responds with HTTP 409 and a list of blocking tasks. Drain or migrate those tasks first (transition them to a terminal state, or cancel them).
  • Adding new transitions is always safe.
  • Removing transitions is safe as long as no task is mid-transit through the removed edge.

Pause the project (PATCH projects/{id} {paused_at: …}) before a major workflow rewrite to keep new agent claims out while you migrate.

Workflow template library

Forge ships built-in templates so you don’t have to start from scratch. On startup Forge writes them to {data_dir}/workflows/:

TemplateDescription
defaultFull default workflow with planning and review gates. Human approval required for the planning gate.
user-approval-reviewLike default, but reviewer is configured for human approval.
no-user-approvalLike default, but planning and review gates auto-cascade without human approval.

You can also write your own YAML template files into that directory. Template names must match [a-z0-9][a-z0-9_-]{0,63}.

Applying a template

POST /api/v1/workflow_templates/{name}/apply
{ "project_id": "…" }

Applying a template snapshots its definition into the project — subsequent edits to the template do not propagate to projects that have already applied it. If you want to keep a project in lockstep with a template, write your own sync workflow on top of the API.

The default template cannot be deleted via the API, but it regenerates automatically on server startup if the file is missing.

Workflow vs project settings

A workflow definition controls states, transitions, and roles.

A project’s lifecycle hooks, default review config, redaction patterns, and similar runtime knobs live in project settings and are edited separately. Workflow updates don’t blow those away, and vice versa.