Workflow Registry
src/cai/workflows/registry.py is the single source of truth for every cai CLI workflow. Each entry in the WORKFLOWS list describes one user-facing cai-* command — its metadata drives docs generation, CI YAML generation, GitHub event routing, and Langfuse session tracking.
WorkflowSpec
A frozen dataclass (WorkflowSpec) captures every piece of metadata downstream tooling needs:
| Field | Type | Purpose |
|---|---|---|
slug | str | URL-safe identifier ("solve", "audit"). Used for doc filenames and CI YAML names. |
title | str | Human-readable title ("CAI Solve"). Appears in Just-the-Docs nav and CI workflow name. |
nav_order | int | Position in the “Workflows” nav. Must be unique across the registry. |
blurb | str | Description rendered at the top of the generated docs page. |
graph | pydantic_graph.Graph | The Graph instance whose nodes and edges produce the mermaid diagram. |
cli_entry | str | Import path ("cai.workflows.solve:main") for the CLI entry point. Must be listed in [project.scripts] in pyproject.toml. |
session_id | Callable[..., str] | Callable that returns a Langfuse session ID (see Langfuse session ID). |
github_trigger | GitHubTrigger | Trigger configuration for the generated GitHub Actions YAML (on: block and optional job_if). |
docker_command | str | Command run inside the container by the CI job. May include $ expressions. |
permissions | dict[str, str] | GitHub Actions job permissions (e.g. {"contents": "write"}). Must be non-empty. |
concurrency_group | str \| None | Concurrency group expression to prevent overlapping runs. None when no concurrency control is needed. |
authorized_user_variant | str | Controls which users can trigger the workflow. One of: "standard", "skip_bots", "none". |
Supporting types:
GitHubTrigger— holds anonlist ofGitHubTriggerEventobjects plus an optionaljob_ifconditional for the job-level guard.GitHubTriggerEvent— one trigger event ("issues","workflow_dispatch","schedule","push","workflow_run", or"pull_request_review") with optionaltypes,branches,workflows,cron, andinputs.
Generators
Two scripts consume the registry. Both run as pre-commit hooks (.pre-commit-config.yaml) so generated files stay in sync automatically.
scripts/gen_workflow_graphs.py
Produces two kinds of docs under docs/workflows/:
index.md— the parent page withhas_children: true, listing all child pages automatically via Just-the-Docs.{slug}.md— one page per spec, containing the blurb and a mermaid diagram rendered fromspec.graph.mermaid_code().
Every commit that touches src/cai/workflows/ re-runs this script; if the output changes, pre-commit fails, re-stages the regenerated files, and the commit proceeds with up-to-date diagrams.
scripts/gen_github_workflows.py
Produces .github/workflows/cai-{slug}.yml for every spec by rendering the Jinja template scripts/templates/cai_workflow.yml.j2 with the spec’s fields.
Workflows whose CI YAML needs to be hand-written (or should not exist) can be skipped by adding their slug to the SKIP_SLUGS set at the top of this script. Currently: {"audit-auto", "memory-audit"}.
Langfuse session ID
The session_id field is a callable — not a string — so session IDs are resolved at run time with access to contextual data like issue numbers and branch names.
_solve_session_id(number, branch=None)— returns"issue-{number}"for issue runs. When a branch is supplied (PR path), delegates tosession_id_for_prinsrc/cai/log/observability.py._audit_session_id()— returns"audit-{YYYYMMDD-HHMMSS}"._sourcing_session_id()— returns"sourcing-{YYYYMMDD-HHMMSS}"._memory_audit_session_id()— returns"memory-audit-{YYYYMMDD-HHMMSS}".session_id_for_pr(pr_number, branch)— matchescai/solve-{issue}branch names to group PR reviews under the original issue session; falls back to"pr-{n}"for human-created PRs.
This grouping means an issue-solving run, its resulting PR’s review-thread runs, and any later conflict resolution all share one Langfuse session.
How to add a new workflow
Follow these steps to wire a new cai-* CLI through the registry.
1. Write the graph
Create a pydantic_graph.Graph with your BaseNode subclasses. Model it after the existing graphs in src/cai/workflows/ (e.g. solve_graph in fsm.py, audit_graph in audit.py).
2. Add a WorkflowSpec entry
Insert a WorkflowSpec(...) into the WORKFLOWS list in src/cai/workflows/registry.py. Fill every field:
WorkflowSpec(
slug="my-workflow",
title="CAI My Workflow",
nav_order=9, # next unused number
blurb="Short description of what this workflow does.",
graph=my_workflow_graph,
cli_entry="cai.workflows.my_workflow:main",
session_id=_my_workflow_session_id,
github_trigger=GitHubTrigger(
on=[GitHubTriggerEvent(event="workflow_dispatch")],
),
docker_command="cai-my-workflow",
permissions={"contents": "read"},
)
nav_ordermust be unique — pick the next available integer.slugmust be unique — it determines the doc filename and CI YAML name.session_idmust be a callable returning astr.permissionsmust be a non-emptydict[str, str].authorized_user_variantmust be"standard","skip_bots", or"none".
3. Add the CLI entry point
Add an entry to [project.scripts] in pyproject.toml:
cai-my-workflow = "cai.workflows.my_workflow:main"
4. Skip GitHub Actions generation (only if needed)
If your workflow should not have a generated CI YAML (or needs a hand-written one), add its slug to SKIP_SLUGS in scripts/gen_github_workflows.py. Otherwise the generator handles it automatically — no extra step required.
5. Run the generators
python scripts/gen_workflow_graphs.py
python scripts/gen_github_workflows.py
Or commit and let the pre-commit hooks regenerate the files. Either way, the generators produce:
docs/workflows/my-workflow.md.github/workflows/cai-my-workflow.yml
6. Commit everything
Commit the registry entry, the graph module, the pyproject.toml change, and all generated files.
Sub-graphs are not in the registry
Modules like src/cai/workflows/explore.py, refine.py, and implement.py define BaseNode subclasses that compose solve_graph in fsm.py. They are internal graph nodes — not standalone CLI entry points — and are intentionally excluded from the registry. The registry only catalogs top-level graphs that map to a cai-* command.
Tests
tests/workflows/test_registry.py verifies registry invariants. Adding a new spec requires these tests to still pass. Key assertions:
- Unique
slugvalues - Unique
nav_ordervalues - Every
cli_entryis importable viaimportlib - Every
session_idis callable - Every
permissionsdict is non-empty with string keys and values - Every
authorized_user_variantis one of{"standard", "skip_bots", "none"} - Every
graphis apydantic_graph.Graphinstance - Every
github_trigger.onlist is non-empty - Every
docker_commandis a non-empty string