Workflows
Workflows
Section titled “Workflows”Colony pipelines are defined as workflows — YAML state machines that control how issues move through the pipeline. Colony ships with one built-in workflow (colony-default) that covers the standard analyze → develop → review → merge lifecycle. You can replace it with a custom workflow per-tenant.
YAML schema
Section titled “YAML schema”Every workflow file must satisfy schema_version: 1. The top-level keys are:
schema_version: 1
workflow: id: my-workflow # kebab-case slug, unique identifier name: 'My pipeline' # human-readable label version: 1 # positive integer, incremented on each change description: '...' # optional unit: # optional; names the item type moving through this workflow singular: issue # default: "issue" plural: issues # default: "issues"
intake: initial_state: new # slug of the first state issues enter trigger_label: colony:enqueue # optional GitHub label that seeds an issue
states: <slug>: # ... per-state fields
operator_actions: <slug>: # ... per-action fieldsState slugs
Section titled “State slugs”State and action keys must be kebab-case slugs: lowercase letters, digits, and hyphens; 1–64 characters; must start with a letter or digit.
States
Section titled “States”The states key is a record of slug → state definition. Each state has the following fields:
| Field | Required | Description |
|---|---|---|
type | yes | State trait — see State traits |
executor | no | Which executor to run — see ExecutorRef |
recovery | yes (if type: blocked) | How the state recovers: manual or auto |
recovery_target | no | Re-entry state for retries/auto-unblocks (only valid on type: blocked; must reference a declared executor-bearing state). When omitted, the engine falls back to the analyze/develop heuristic. |
label | no | GitHub label string for this state (cosmetic) |
on | no | Outcome → route map — see Outcome routing |
transitions | no | Optional additive array of state slugs that expand the effective allowed-transition set. The effective set is derived from on: targets, recovery_target, operator action to-targets, and state traits (paused, blocked, unlabeled); transitions: declares any edges that are genuinely additive and not otherwise derivable. |
hooks | no | Terminal-only post-completion hooks — see Hooks |
State traits
Section titled “State traits”The type field controls the lifecycle semantics of a state:
| Trait | Meaning |
|---|---|
active | Worker picks up a task and runs the state’s executor |
blocked | Issue is blocked; must declare recovery: manual or recovery: auto |
awaiting-human | Issue is waiting for human input; no executor runs |
terminal | Issue is complete; must not declare executor or transitions |
ExecutorRef
Section titled “ExecutorRef”The executor field takes a reference string in one of two forms:
builtin:<name> # e.g. builtin:analyze, builtin:developplugin:<package>/<name> # e.g. plugin:colony-content/analyzeBuilt-in executors are provided by Colony itself. Plugin executors are declared by a registered plugin — see Plugins.
Outcome routing
Section titled “Outcome routing”The on key maps executor outcome names to state transitions. Each value is either a plain slug (shorthand) or an explicit route object:
on: success: ready-for-dev # shorthand: target state slug escalate_to_planning: target: planning # explicit object form outputs: # optional metadata forwarded to the next state upstream_outcome: scope_overflowThe key noop is reserved and must not appear in on:. The framework handles noop returns without transitioning the issue.
All target slugs in on: must reference states defined in the same workflow definition.
hooks may only be declared on terminal states. They trigger out-of-band tasks after the issue reaches the terminal state:
done: type: terminal hooks: - task_type: retrospect executor: builtin:retrospect optional: true # if true, failure does not block the terminal transition once_per_issue: true # if true, the hook only fires once regardless of rerunsOperator actions
Section titled “Operator actions”The operator_actions key registers named actions that pipeline operators (humans or MCP tools) can trigger via slash commands. Each action has:
| Field | Required | Description |
|---|---|---|
description | yes | Human-readable label for the action |
framework | no | true marks this as a framework-reserved action (no from/to required) |
from | yes (non-framework) | Array of source state slugs from which this action is valid |
to | yes (non-framework) | Target state slug |
side_effects | no | Side effects to execute: close-pr, add-label, remove-label |
Non-framework operator actions must declare both from and to. Framework actions (e.g. retry) are handled by the engine and do not require routing fields.
operator_actions: retry: description: "Re-enqueue current state's executor after failure." framework: true reimplement: description: 'Discard PR; return to development.' from: [in-review, merge-pending] to: ready-for-dev side_effects: [close-pr]All from and to slugs in operator actions must reference defined states.
Runtime semantics
Section titled “Runtime semantics”At runtime, operator_actions is the single source of truth for how slash commands (/colony:reimplement, /colony:reanalyze, /colony:reopen) behave. The workflow snapshot is read on every invocation — changes to operator_actions take effect the next time the workflow is loaded.
to — target state
to is the state the issue transitions to when the slash command runs. For example, reimplement.to: analyzing means /colony:reimplement sends the issue back to the analyzing state.
from — valid source states
from declares the states an action may be invoked from. Its enforcement differs by command:
/colony:reopenenforcesfromstrictly. If the issue is not in a listed state, Colony posts an error comment (“Cannot reopen: issue is in<state>, but/colony:reopenis only available from:<allowed states>”) and the transition does not happen./colony:reimplementand/colony:reanalyzedo not enforcefromwith a dedicated error comment. Instead, the effective allowed-transition set (states[from].allowedTransitions, derived fromon:,recovery_target, operator action targets, state traits, and any additivetransitions:entries) governs whether the target state is reachable from the current state. Forreimplement, branch and worktree cleanup runs before the transition attempt regardless of whether the transition ultimately succeeds.
Undeclared actions
retry and reopen are the operator-action spine — the framework guarantees both are available even when a workflow declares neither. If a workflow omits them, they are supplied automatically from trait-derived defaults (retry: framework-supplied, no from/to; reopen: from the workflow’s terminal states, to intake.initial_state). A workflow may rename a spine action (reopen is resolved by intent: reopen as well as by name), but it cannot redeclare one with different semantics — that fails validation at publish time, naming the action.
For any other action (e.g. reimplement, reanalyze), if a workflow does not declare it at all, the corresponding slash command replies with “This workflow does not support /colony:<action>” and returns without making any changes.
side_effects: [close-pr]
When close-pr appears in a non-framework action’s side_effects, the sprint-master closes any open PR for the issue before the state transition. Without close-pr, the PR is left open. For reimplement in colony-default the PR is always closed; for a workflow that omits side_effects, the PR stays open.
Slash-command alias resolution
/colony:reimplement resolves to the workflow action named reimplement first. If the workflow does not declare reimplement but does declare redraft (as colony-content does), the runtime falls back to redraft. This allows content-pipeline workflows to use their own action name while the operator always types /colony:reimplement. The alias order is ['reimplement', 'redraft'] — the first declared candidate wins.
Terminal-state exit for reopen
reopen is the supported mechanism for operators to exit a terminal state (done). Terminal states have no outgoing transitions in the workflow graph, so a normal state transition would be rejected. The runtime uses allowTerminalExit: true specifically for reopen to bypass this check. Because reopen is a spine action, a workflow does not need to declare it — the framework supplies a default targeting intake.initial_state from every state of trait terminal. A workflow only needs to declare reopen explicitly in order to rename it (via intent: reopen), attach side_effects, or narrow which terminal states support it; the declared to must still match intake.initial_state, and the declared from must be a non-empty subset of the workflow’s terminal-trait states (a workflow with two terminal states, e.g. done and cancelled, may declare reopen with from: [done] to support reopening only from done) — declaring from with a state that is not terminal, or an empty from, fails publishing with a message naming the action. A workflow with no terminal-trait state at all is exempt — there is no terminal-state set to derive from: from, so the spine does not supply reopen in that case, and /colony:reopen resolves to not-declared.
Triggers
Section titled “Triggers”The optional top-level triggers array declares out-of-band task enqueues that the engine should initiate for issues in specific states. This is the data-driven equivalent of hardcoded sweep logic (e.g. enqueuing ci_repair whenever an issue is blocked with block_reason: ci_hard_failure).
| Field | Required | Description |
|---|---|---|
task_type | yes | Task type string to enqueue (e.g. ci_repair) |
state | yes | Kebab-case slug of the state this trigger applies to (typically a blocked state) |
block_reason | no | Predicate matched against the issue’s block_reason column; trigger only fires when the column value equals this string |
executor | no | Executor ref (builtin:<name> or plugin:<pkg>/<name>) whose outcomes the enqueued task will produce |
max_cycles | no | Positive integer cap on re-enqueue attempts (default mirrors today’s value of 2) |
triggers: - task_type: ci_repair state: failure-blocked block_reason: ci_hard_failure executor: builtin:ci-repair max_cycles: 2Cross-reference validation (ensuring state and executor reference declared entities) is enforced at workflow-load time, not at parse time. The triggers block is optional and backward-compatible — existing workflows without it are unaffected.
Schema constraints
Section titled “Schema constraints”The schema enforces the following rules at parse time:
intake.initial_statemust reference a defined state.- States of type
blockedmust declarerecovery. recovery_target, if declared, may only appear ontype: blockedstates and must reference a state that declares anexecutor(i.e. a claimable/active-executor state). When omitted, retry and auto-unblock routing falls back to the built-in analyze/develop heuristic.- States of type
terminalmust not declareexecutorortransitions. noopmust not appear as a key in any state’son:map.hooksmay only be declared onterminalstates.- Every target in
on:, any additivetransitions:entries, and operator actionfrom/tofields must reference a defined state.
Built-in workflow: colony-default
Section titled “Built-in workflow: colony-default”Colony ships with colony-default, the standard analyze → develop → review → merge pipeline. It is the default workflow for all tenants unless overridden.
Key properties:
intake.trigger_label: colony:enqueue— adding this label to a GitHub issue seeds it into the pipeline. See First Issue for details.intake.initial_state: new— issues enter thenewstate, wherebuiltin:analyzeruns.- States:
new,planning,analyzing,needs-clarification,ready-for-dev,dependency-blocked,failure-blocked,changes-requested,in-review,merge-pending,human-review-ready,waiting-for-subtasks,done,paused,unlabeled. Thedependency-blockedpipeline state (IssueState.DependencyBlocked) and thecolony:blockedflag label are unchanged. Dependency blocks are surfaced in GitHub as native blocked by #N relationships (via the issue-dependencies API) rather than acolony:dependency-blockedlabel — the label is no longer projected. - Terminal state:
done— triggers aretrospecthook (optional, once-per-issue). - Operator actions:
retry(framework),reimplement,reanalyze,reopen.
To view the full YAML:
colony workflow show --builtin colony-defaultBuilt-in workflow: colony-support
Section titled “Built-in workflow: colony-support”Colony ships with colony-support, a first-line support responder for handling customer or user questions. It produces no PR — it only posts responses, requests clarification, and confirms resolution.
Intake model
Section titled “Intake model”colony-support is designed for a dedicated support repo where every new issue is a support request. To activate it:
- Set
intake_mode: allon the sprint-master so every new issue enters the pipeline automatically (rather than requiring acolony:enqueuelabel). - Add an
intake_ruleslabel rule so that issues labeledsupportare routed to thecolony-supportworkflow; all other issues fall through tocolony-default.
agents: sprint_master: intake_mode: all
intake_rules: default: colony-default rules: - workflow: colony-support match: labels: [support]State machine
Section titled “State machine”new ──(answered)──► awaiting-confirmation ──(confirmed_resolved)──► done ▲ │ │ (confirmed_unresolved) └──────────────────────────┘
new ──(needs_clarification)──► needs-clarification ──(clarification_received)──► newnew ──(escalate)──► escalatednew ──(failure)──► failure-blocked| State | Type | Description |
|---|---|---|
new | active | plugin:colony-support/respond runs: grounding lookup → answer posted |
awaiting-confirmation | awaiting-human | Waits for the user to check the confirmation checkbox or reply |
needs-clarification | awaiting-human | Bot asked a clarifying question; re-enters new when answered |
escalated | awaiting-human | Routed to a human; no automatic resolution |
failure-blocked | blocked (manual) | Executor error; /colony:retry re-enters new |
done | terminal | Issue resolved |
The confirmation-reply loop in the sprint-master polls awaiting-confirmation issues, detects a checked “Yes, this resolved my issue” checkbox or a positive reply, and transitions to done. An unchecked or negative reply transitions back to new for another attempt (up to a maximum re-answer limit, after which the issue is escalated).
Grounding
Section titled “Grounding”Place FAQ and policy documents in a .colony/support/ directory inside the support repo. The respond executor picks up all Markdown files in that directory as grounding context before generating an answer.
An optional read-only reference submodule can be pinned in the support repo at the operator level to bring in documentation from another repository (e.g. product docs, an internal knowledge base). This submodule is used only as static grounding content on the host filesystem — it is never checked out inside an agent worktree, which keeps it compatible with the repo-wide constraint that submodules are not supported in agent workspaces.
colony-support does not create branches, worktrees, or pull requests. Every transition is a comment or label change on the original issue.
colony workflow show --builtin colony-supportValid --builtin values: colony-default, colony-content, colony-support.
Custom workflow example
Section titled “Custom workflow example”The following illustrates a lean pipeline for content/documentation issues using two plugin executors. This is the minimal workflow that covers all outcomes declared by the @colony/plugin-colony-content executors (analyze: success, needs_clarification, failure; draft: draft_created, needs_clarification, failure).
schema_version: 1
workflow: id: colony-content version: 1 name: 'Content pipeline' description: 'Lean workflow for content/docs. No planning, no CI gating.'
intake: initial_state: new trigger_label: colony:enqueue
states: new: type: active executor: plugin:colony-content/analyze on: success: drafting needs_clarification: needs-clarification failure: failure-blocked
drafting: type: active executor: plugin:colony-content/draft on: draft_created: human-review-ready needs_clarification: needs-clarification failure: failure-blocked
needs-clarification: type: awaiting-human on: clarification_received: new transitions: [drafting] # additive: drafting not covered by on: or trait rules
human-review-ready: type: awaiting-human transitions: [drafting, done, failure-blocked] # no on: entries; all edges are additive
failure-blocked: type: blocked recovery: manual recovery_target: new # retry/auto-unblock re-enters at 'new' instead of the analyze/develop heuristic # transitions: omitted — blocked/manual trait rule derives all reachable states automatically
done: type: terminal
operator_actions: retry: description: 'Retry the last failed step.' framework: trueThis workflow uses plugin:colony-content/analyze and plugin:colony-content/draft — executors provided by the colony-content plugin. See Plugins for how to author and register a plugin.
CLI reference
Section titled “CLI reference”colony workflow validate <file>
Section titled “colony workflow validate <file>”Parse and validate a workflow YAML file against the schema. Exits 0 on success, 2 on parse or validation error.
$ colony workflow validate ./my-workflow.yamlOK: my-workflow v1 states: 9 operator_actions: 2 content_hash: a3f1...validate checks the Zod schema and all cross-reference constraints (target states exist, blocked states declare recovery, etc.). It does not verify that every executor outcome declared by a plugin is handled in the workflow’s on: block — that check runs at workflow-load time when the worker starts.
colony workflow show
Section titled “colony workflow show”Print a workflow definition as YAML. Two modes:
Built-in workflow (no database required):
colony workflow show --builtin colony-defaultValid --builtin values: colony-default, colony-content, colony-support.
Stored workflow (reads from the registry database):
colony workflow show \ --workflow-id my-workflow \ --version 2 \ --tenant-id 42 \ [--database-url postgres://...]All three of --workflow-id, --version, and --tenant-id are required for registry reads. The database URL is read from --database-url or the $DATABASE_URL environment variable. The command exits 2 if neither mode is fully specified.