Workflow

Added in version 0.31.0.

The [workflow] section declares a project’s process model — the ordered stages a requirement passes through, the traceability each stage must satisfy, and the lifecycle states a need may take. It is consumed by the ubc agent workflow engine, which evaluates the declared gates against your needs and reports where the process is incomplete.

Caution

The [workflow] section drives the ubc agent engine, which is an evolving feature. The keys below are stable enough to author against (they are validated by the schema and rejected on a typo), but new keys may be added as the engine grows.

See also

This page is the key-by-key reference for [workflow] and the sibling [agent.runner] table. For the concepts, a quick start, the ubc agent command, and the VS Code surfaces, see the Pharaoh Agentic Engineer guide — in particular its configuration walkthrough.

Unlike the other sections, [workflow] is read only by ubc agent — it does not affect ubc build or ubc check, and it is not part of a project’s reproducible build hash. It still inherits through extend and responds to -c overrides like every other section.

Every key is validated: an unknown key, or an invalid value for one of the closed-set fields (such as a stage route scope or a trace direction), is a hard error rather than a silently-ignored setting.

Minimal example

[workflow]
exempt_status = ["deprecated"]

[[workflow.stages]]
id = "requirements"
produces = "requirement"

[workflow.stages.route]
scope = "stream"
granularity = "per-root"
path = "specs/{stream}/requirements.rst"

[[workflow.stages.trace]]
up = "user_story"
link = "implements"

Top-level options

exempt_status

Type: array of string (default: [])

Statuses whose needs are dropped before coverage is computed. A need in one of these states is ignored by the process gates, so a deprecated or descoped item does not count as an open gap.

release_disallow_status

Type: array of string (default: [])

Statuses that are forbidden at release time. A need left in one of these states blocks a release check.

orphan_exempt_types

Type: array of string (default: [])

Need types excluded from the orphan check only. A type listed here may exist without an incoming trace without being reported as orphaned.

Lifecycle

The [workflow.lifecycle] sub-table declares the legal status values a need may take, in order, and (optionally) the legal transitions between them.

[workflow.lifecycle]
states = ["open", "in_progress", "done"]
transitions = [
    { from = "open", to = "in_progress" },
    { from = "in_progress", to = "done" },
]
states

Type: array of string (default: [])

The declared lifecycle states, in order. The first entry is the state a new need opens in. When non-empty, a need whose status is not one of these values is reported by the status_undeclared gate. An absent table, or an empty list, keeps that gate off.

transitions

Type: array of table or pair (default: [])

The legal state transitions. Each entry may be written either as a table with from and to keys, or as a positional two-element [from, to] pair, so you can use whichever form reads better. When present, ubc agent config-validate cross-checks every from/to against states, and also checks that states and transitions agree on which state is first. If exactly one declared state is never a transition target and it is not states[0], that is a contradiction between states and transitions (either the order is wrong, or a transition into that state is missing, and the check cannot tell which), and ubc agent config-validate exits non-zero. ubc agent doctor reports the same finding as its config-validity check, at error. Native ubc check does not run this cross-check (see the note at the top of this page).

[workflow.lifecycle]
states = ["open", "done"]
transitions = [
    { from = "open", to = "done" },   # table form
    ["open", "done"],                 # positional form
]

Diagram gate

The [workflow.diagram] sub-table configures the diagram gate, which validates diagram blocks in authored needs.

[workflow.diagram]
enabled = true
engine = "mermaid"
enabled

Type: boolean (default: true when the table is present)

Whether the diagram gate is enabled. A present [workflow.diagram] table is opt-in, so enabled defaults to true; set enabled = false to keep the table parsed but the gate off.

engine

Type: string (default: "mermaid")

Which diagram engine the gate validates against. One of:

  • mermaid — validate Mermaid blocks.

  • plantuml — validate PlantUML blocks.

  • both — validate both engines.

format

Type: string (default: none)

The diagram syntax the agent should author any diagram as (for example mermaid or plantuml). This is independent of enabled and engine: it is an authoring hint, so a project can pin a diagram format for authoring even when the diagram gate itself is off.

Stages

Each [[workflow.stages]] entry is one step of the process, in declaration order. A stage produces a need type and declares the gates that step must satisfy.

id

Type: string (required)

The stage identifier, referenced by other stages’ depends_on.

produces

Type: string (default: none)

The need type this stage produces. A stage with no produces is a code stage — it authors no need and is gated solely by its require_code gate (which must then carry an explicit type).

depends_on

Type: array of string (default: [])

The ids of stages this stage depends on. A stage whose dependencies are unmet is reported as blocked.

author_skill

Type: string (default: none)

The name of the agent skill used to author this stage.

review_skill

Type: string (default: none)

The name of the agent skill used to review this stage.

authoring

Type: string (default: none)

A project-specific authoring directive for this stage (free text). It is injected into the agent’s authoring prompt, so process policy — for example “decompose into the full set of requirements” — lives in config rather than in the generic skill prose.

optional

Type: boolean (default: false)

Whether this stage is informational-only, never a required blocking step in the dependency chain. An optional stage is never forced as blocked or as the next step; it surfaces only when the agent catches it during the process.

Stage route

The [workflow.stages.route] sub-table declares where a stage’s needs are authored.

[workflow.stages.route]
scope = "stream"
granularity = "per-root"
path = "specs/{stream}/requirements.rst"
scope

Type: string (required)

The authoring scope. One of:

  • stream — authored under a per-stream subtree.

  • global — authored at one global location.

Scope also decides what an authoring run is briefed with. A stream route (or any path still carrying {stream}) is briefed with the active stream’s needs, a global one with the whole graph.

granularity

Type: string (required)

The authoring granularity. One of:

  • global — one authoring run for the whole graph.

  • per-need — one authoring run per produced need.

  • per-root — one authoring run per trace-chain root and its subtree.

  • single — a single authoring run.

path

Type: string (required)

The authoring path template. Supports the {stream} and {id} placeholders.

group_field

Type: string (default: none)

An optional grouping field.

Stage code gate

The [workflow.stages.require_code] sub-table declares a field-presence gate: every need of the gated type must carry at least min non-empty values in field.

[workflow.stages.require_code]
field = "code_url"
min = 1
type

Type: string (default: the stage’s produces)

The gated need type. For a need-producing stage it defaults to the produced type; for a code stage (a stage with no produces) it must be set explicitly.

field

Type: string (default: "code_url")

The need field the gate ranges over.

min

Type: integer (default: 1)

The minimum number of non-empty values required in field.

Note

The gated field (by default code_url) is populated by the codelinks need-id reference mechanism — an @need-ids: marker in source that back-attaches a URL onto the existing need it names. One-line need markers do not set this field; they materialise a new need carrying a trace link instead. To gate coverage against one-line-materialised impl / test needs — as the vmodel profile does — use a bidirectional trace edge on the produced type rather than require_code.

Stage trace edges

Each [[workflow.stages.trace]] entry declares a traceability edge: the produced type must trace up to up via link.

[[workflow.stages.trace]]
up = "user_story"
link = "implements"
direction = "both"
min = 1
up

Type: string (required)

The upstream need type the produced type traces up to.

link

Type: string (required)

The link name the trace edge follows.

direction

Type: string (default: "both")

The edge direction. One of:

  • both — bidirectional: the child must trace up via link to a need of the up type, and every up need must be covered down by a need of the type the stage produces. A link whose far end is a need of some other type does not count toward either leg.

  • incoming — the parent type must carry the incoming link. This direction builds no trace edge today, so it raises no trace gap of its own. The stage still declares a trace gate and still owns orphan gaps on the up type.

  • outgoing — the produced child must carry the outgoing link. This direction builds no trace edge today, so it raises no trace gap of its own. The stage still declares a trace gate.

min

Type: integer (default: 1)

The minimum required link count. Under both, only links whose far end carries the required type count toward it. Under incoming or outgoing no trace gate counts it, so it is only the number the authoring prompt dictates to whoever writes the need.

Icons

The [workflow.icons] table maps a need type name to the icon it renders with in the workflow view. A type not listed here uses a default icon, chosen by matching its name against a built-in alias table — for example an impl or implementation type renders with the code icon (not a distinct one), and a config or configuration type with the settings icon.

[workflow.icons]
requirement = "req"
architecture = "arch"
verification = "test"

The value must be one of the following presentation-neutral icon names. The name is intentionally decoupled from your own type names, so you can — for example — render a software_requirement type with the req icon. The complete set, each shown with the icon it currently renders as:

Icon

Name

Depicts

feature

A user-facing feature, user story, or epic.

req

A requirement (system, software, or component level).

arch

An architecture or design element.

code

A source-code artefact.

test

A test or verification case.

spec

A specification document.

risk

A risk or FMEA entry.

decision

A decision record.

task

A task or work item.

fsr

A functional safety requirement.

hazard

A hazard or HARA entry.

safety_goal

A safety goal.

release

A release or milestone.

people

A person, team, or role.

interface

An interface, API, or port.

component

A component, module, or block.

settings

A settings or configuration element.

Note

The glyphs are the VS Code codicons the workflow view currently maps each name to. The names are the stable configuration contract; the concrete glyph behind a name may be refined between releases.

Complete example

[workflow]
exempt_status = ["deprecated"]
release_disallow_status = ["open"]
orphan_exempt_types = ["release"]

[workflow.lifecycle]
states = ["open", "in_progress", "done"]
transitions = [
    { from = "open", to = "in_progress" },
    { from = "in_progress", to = "done" },
]

[workflow.icons]
requirement = "req"
architecture = "arch"

[[workflow.stages]]
id = "requirements"
produces = "requirement"
author_skill = "author-requirement"

[workflow.stages.route]
scope = "stream"
granularity = "per-root"
path = "specs/{stream}/requirements.rst"

[[workflow.stages.trace]]
up = "user_story"
link = "implements"

[[workflow.stages]]
id = "architecture"
produces = "architecture"
depends_on = ["requirements"]

[workflow.stages.route]
scope = "stream"
granularity = "per-root"
path = "specs/{stream}/architecture.rst"

[[workflow.stages.trace]]
up = "requirement"
link = "implements"

The [agent.runner] table

ubc agent run executes a workflow stage through a headless AI process, configured by the [agent.runner] table — a sibling of [workflow] in ubproject.toml:

[agent.runner]
command = "claude -p --permission-mode acceptEdits --output-format stream-json --verbose"
model = "sonnet"
timeout_secs = 600
command

Type: string (no default)

The runner invocation. The assembled prompt (recommended skill + context briefing + resolved route) is passed on the process’s stdin. There is no built-in default and the shipped preset ships none, so with the key absent no runner is configured and ubc agent run reports that rather than spawning a tool you did not name. ubc agent prompt assembles the same prompt and prints it on stdout rather than spawning anything, so it reads no key of this table. It prefixes the printed text with a === WORKING DIRECTORY === header, since a reader of that text does not inherit the runner’s working directory.

model

Type: string (optional)

A model to pin for the runner (for example sonnet for routine authoring).

timeout_secs

Type: integer (default: 600)

The wall-clock budget for one authoring pass, in seconds. A runner that does not exit within the budget is killed; 0 disables the bound.

Each key can be overridden per invocation with the UBC_AGENT_RUNNER, UBC_AGENT_RUNNER_MODEL, and UBC_AGENT_RUNNER_TIMEOUT environment variables, which take precedence over the table.

Note

[agent.runner] only affects the headless ubc agent run path. Driving the loop interactively from your editor agent (Copilot, Claude Code, Mistral Vibe, or another) uses that agent directly and ignores this table.

Note

The [agent] table is strict, so a key it does not declare (under [agent.runner], [agent.forge] or [agent.reverse]) fails the ubc agent commands that read the table, with the file and the line the key is on. No other command reads it, so such a key does not stop ubc build, ubc check or the language server. [agent.forge] has its own reference section below, beside [agent.reverse].

The [agent.forge] table

ubc agent reverse reads the git history of the code it recovers from, to report the issues and pull requests that explain why that code exists. Those live on your forge, the service that hosts the repository, such as GitHub, GitLab or Bitbucket, which is what the table is named after. The [agent.forge] table states the facts about your git remote that the remote URL itself cannot carry, and is a sibling of [agent.runner] above:

[agent.forge]
host = "git.example.com"
system = "gitlab"
jira_keys = ["DCA11Y"]
host

Type: string (optional)

The host this declaration is about. Unset, it applies to whatever host the origin remote carries, which is the common case, since a project has one origin. Set, it applies only to that host, and a remote on any other host is classified by its host name alone. A port in the value is ignored when the hosts are compared.

system

Type: string (optional)

Which forge software that host runs: github, gitlab or bitbucket. This is the one fact a URL cannot carry, because a self-managed GitLab on a corporate domain looks exactly like a Bitbucket Server one. Unset, the system is derived from the host name, which recognises the three cloud hosts and the conventional github. / gitlab. / bitbucket. first label. A value that is none of the three names is ignored rather than fatal, so a project naming a system this ubc does not know still gets the host rule.

jira_keys

Type: list[string] (default: empty)

The Jira project keys that may be read out of commit text, as an allowlist. Unset, the Jira pattern is never emitted at all, so no Jira reference ever reaches a recovery brief. It is an allowlist rather than a hint because the bare pattern matches any capitalised word followed by a hyphen and digits, so UTF-8, BSD-3 and SHA-256 all read as issue keys on a repository that tracks no Jira project. Naming your own keys is what makes every extracted reference a real one. An extracted key reaches the recovery brief as a bare identifier (origin = "jira-key", no host) and is handed over unresolved by design. Nothing in [agent.forge] names a Jira instance, since host and system describe the git remote, and no issues skill can be routed to Jira. A recovery run resolves a key only when issues is selected and an installed skill covers Jira. Otherwise the key stays an identifier: it is reported as unresolved, never looked up and never filled in from the surrounding commit.

Note

This table is read for DETECTION only. A named system means “a skill for this system is the one that applies here”, never that the host is reachable, that a credential exists, or that any tool is installed.

The [agent.reverse] table

ubc agent reverse reconstructs the decomposition arm of the V-model from an existing codebase. The [agent.reverse] table configures what it may do to a repository it did not author, and is a sibling of [agent.runner] above:

[agent.reverse]
code_stage = "implementation"
requirement_stage = "spec"
write_code_markers = false
code_stage

Type: string (default: "code")

The id of the stage the reverse ladder climbs from, the stage whose route holds the code. Set it when your workflow’s code anchor is not called code, so ubc agent reverse sources / plan / status climb from the right stage without --code-stage on every invocation.

requirement_stage

Type: string (default: none)

The id of the stage recovery extracts requirements into. Set it only when that stage’s need type is not spelled req or requirement. Without it, recovery takes the stage producing req or requirement above the code stage, else the nearest need-producing stage above it, and says so in the note of ubc agent reverse sources. An id that names no need-producing stage above the code stage is ignored, and ubc agent reverse sources names it in the same note.

write_code_markers

Type: bool (default: true)

Whether recovery may write codelinks markers into your source files.

With it on, recovering the stage below your requirements writes one one-line marker into each file of each recovered module that its codelinks project parses (its include, exclude and gitignore apply). The marker goes at the top of the file, after any line the language needs first, such as a shebang (#!) or a Python encoding declaration. Those markers materialise the needs that link the code back up to the element recovered from it, which is what closes the gate below that element.

A marker materialises only where a .. src-trace:: directive names its codelinks project in a built document. Recovery authors that directive into the document it writes when no document has one yet, and never a second one, since two directives for one project materialise every marker twice.

The recovery skill proposes the file list and the ids before writing and waits for an answer. A run with no interactive user writes them and reports which files now carry one.

Set it to false and no brief carries a marker contract at all, so nothing is written into your source. The recovered elements are still authored, still recorded and still correct, and they carry no down-link to the code.

The three [agent] sub-tables inherit across extend one by one, so a project that declares only [agent.runner] keeps a marker opt-out its base set, and a project that declares only [agent.reverse] keeps the base’s runner. A sub-table the project declares replaces the base’s sub-table of the same name entirely.

A -c override merges into [agent] key by key and keeps every key it does not name, so -c 'agent.runner.command = "claude-next"' keeps the model and timeout_secs the file declares. It does not move where ubc agent reverse looks for source: recovery reads its source roots ([codelinks.projects.*] src_dir) and [source] extend_exclude from the files themselves, so change those in ubproject.toml rather than with -c.