Skip to content

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.

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 fields

State and action keys must be kebab-case slugs: lowercase letters, digits, and hyphens; 1–64 characters; must start with a letter or digit.

The states key is a record of slug → state definition. Each state has the following fields:

FieldRequiredDescription
typeyesState trait — see State traits
executornoWhich executor to run — see ExecutorRef
recoveryyes (if type: blocked)How the state recovers: manual or auto
recovery_targetnoRe-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.
labelnoGitHub label string for this state (cosmetic)
onnoOutcome → route map — see Outcome routing
transitionsnoOptional 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.
hooksnoTerminal-only post-completion hooks — see Hooks

The type field controls the lifecycle semantics of a state:

TraitMeaning
activeWorker picks up a task and runs the state’s executor
blockedIssue is blocked; must declare recovery: manual or recovery: auto
awaiting-humanIssue is waiting for human input; no executor runs
terminalIssue is complete; must not declare executor or transitions

The executor field takes a reference string in one of two forms:

builtin:<name> # e.g. builtin:analyze, builtin:develop
plugin:<package>/<name> # e.g. plugin:colony-content/analyze

Built-in executors are provided by Colony itself. Plugin executors are declared by a registered plugin — see Plugins.

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_overflow

The 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 reruns

The operator_actions key registers named actions that pipeline operators (humans or MCP tools) can trigger via slash commands. Each action has:

FieldRequiredDescription
descriptionyesHuman-readable label for the action
frameworknotrue marks this as a framework-reserved action (no from/to required)
fromyes (non-framework)Array of source state slugs from which this action is valid
toyes (non-framework)Target state slug
side_effectsnoSide 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.

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:reopen enforces from strictly. If the issue is not in a listed state, Colony posts an error comment (“Cannot reopen: issue is in <state>, but /colony:reopen is only available from: <allowed states>”) and the transition does not happen.
  • /colony:reimplement and /colony:reanalyze do not enforce from with a dedicated error comment. Instead, the effective allowed-transition set (states[from].allowedTransitions, derived from on:, recovery_target, operator action targets, state traits, and any additive transitions: entries) governs whether the target state is reachable from the current state. For reimplement, 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.

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).

FieldRequiredDescription
task_typeyesTask type string to enqueue (e.g. ci_repair)
stateyesKebab-case slug of the state this trigger applies to (typically a blocked state)
block_reasonnoPredicate matched against the issue’s block_reason column; trigger only fires when the column value equals this string
executornoExecutor ref (builtin:<name> or plugin:<pkg>/<name>) whose outcomes the enqueued task will produce
max_cyclesnoPositive 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: 2

Cross-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.

The schema enforces the following rules at parse time:

  • intake.initial_state must reference a defined state.
  • States of type blocked must declare recovery.
  • recovery_target, if declared, may only appear on type: blocked states and must reference a state that declares an executor (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 terminal must not declare executor or transitions.
  • noop must not appear as a key in any state’s on: map.
  • hooks may only be declared on terminal states.
  • Every target in on:, any additive transitions: entries, and operator action from/to fields must reference a defined state.

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 the new state, where builtin:analyze runs.
  • 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. The dependency-blocked pipeline state (IssueState.DependencyBlocked) and the colony:blocked flag label are unchanged. Dependency blocks are surfaced in GitHub as native blocked by #N relationships (via the issue-dependencies API) rather than a colony:dependency-blocked label — the label is no longer projected.
  • Terminal state: done — triggers a retrospect hook (optional, once-per-issue).
  • Operator actions: retry (framework), reimplement, reanalyze, reopen.

To view the full YAML:

Terminal window
colony workflow show --builtin colony-default

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.

colony-support is designed for a dedicated support repo where every new issue is a support request. To activate it:

  1. Set intake_mode: all on the sprint-master so every new issue enters the pipeline automatically (rather than requiring a colony:enqueue label).
  2. Add an intake_rules label rule so that issues labeled support are routed to the colony-support workflow; all other issues fall through to colony-default.
agents:
sprint_master:
intake_mode: all
intake_rules:
default: colony-default
rules:
- workflow: colony-support
match:
labels: [support]
new ──(answered)──► awaiting-confirmation ──(confirmed_resolved)──► done
▲ │
│ (confirmed_unresolved)
└──────────────────────────┘
new ──(needs_clarification)──► needs-clarification ──(clarification_received)──► new
new ──(escalate)──► escalated
new ──(failure)──► failure-blocked
StateTypeDescription
newactiveplugin:colony-support/respond runs: grounding lookup → answer posted
awaiting-confirmationawaiting-humanWaits for the user to check the confirmation checkbox or reply
needs-clarificationawaiting-humanBot asked a clarifying question; re-enters new when answered
escalatedawaiting-humanRouted to a human; no automatic resolution
failure-blockedblocked (manual)Executor error; /colony:retry re-enters new
doneterminalIssue 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).

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.

Terminal window
colony workflow show --builtin colony-support

Valid --builtin values: colony-default, colony-content, colony-support.

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: true

This 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.

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.yaml
OK: 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.

Print a workflow definition as YAML. Two modes:

Built-in workflow (no database required):

Terminal window
colony workflow show --builtin colony-default

Valid --builtin values: colony-default, colony-content, colony-support.

Stored workflow (reads from the registry database):

Terminal window
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.