onsager-design-workflow
A workflow in Onsager is a trigger plus an ordered chain of stages. The trigger says when to fire; the stages say what to do, in order, until something parks the artifact or releases it. This skill creates and shapes those blueprints. Running a workflow lives in onsager-run-workflow; triaging a stuck run lives in onsager-triage-run.
When this skill triggers
Phrases that should route here:
- "design a workflow that runs on a GitHub issue label"
- "create an automation for merged pull requests"
- "build a pipeline that triages incoming issues"
- "schedule a workflow to run every morning at 09:00"
- "deactivate the morning-digest workflow"
- "list the workflows in this workspace"
If the user is asking to fire an existing workflow ("run this", "trigger a run"), hand off to onsager-run-workflow.
Operating procedure
Step 1 — confirm the workspace
Every tool here takes a workspace_id. If the user hasn't named one, call list_workflows against the workspace they're working in (the chat surface usually has this in scope; otherwise ask). Never invent a workspace id.
Step 2 — pick a trigger
The portal MCP server accepts the trigger kinds defined in onsager-spine::TriggerKind. The wire form is { "kind": "<snake_case_tag>", … } with the variant's fields alongside.
External (webhook) kinds:
github_issue_webhook— fires onissues.labeledwhose label matches. Fields:repo("owner/name"),label.github_pull_request_closed— fires onpull_request.closed. Fields:repo, optionalpredicate.merged(truefor merged-only,falsefor closed-without-merge, omit for any close).github_workflow_run_completed— fires onworkflow_run.completed. Fields:repo,workflow_name, optionalevent,head_branch,conclusion.telegram_webhook— fires on a Telegram bot update. Fields:bot_username, optionalchat_id_allowlist, optionalcommand_prefix(e.g./onsager).
Schedule kinds:
cron— cron schedule. Fields:expression(5- or 6-field cron string), optionaltimezone(IANA name; defaults to UTC).delay— fire once after a delay. Fields:seconds, optionalanchor(v1 onlyworkflow_activated_at).interval— periodic. Fields:period_seconds.
Event kinds:
spine_event— fire on aFactoryEventKindwhosetypematches. Fields:event_kind, optionalfilter.equals(JSON-shape predicate). Forbidden:event_kind: "trigger.fired"self-amplifies and the tool rejects it.pg_notify— fire on a PostgresNOTIFY <channel>. Fields:channel, optionalfilter.outbox_row— fire when a row matchingwhere_clauseis inserted intotable. Fields:table,where_clause.
Manual kinds:
manual— fire on demand. Fields:name(the button label).replay— re-emit a pastTriggerFiredevent by id. Fields:source_event_id.
install_id is the GitHub App installation row id. Required for github_* triggers; pass 0 for schedule / manual / event-bus triggers.
Step 3 — chain stages
Each stage is a { gate_kind, params? } pair. gate_kind is a kebab-case string from onsager-portal::workflow::GateKind:
agent-session— dispatches a stiglab agent session with a prompt.params: { "prompt": "…" }.external-check— an external pass/fail check (e.g. spec-link validation).paramsdefines the check.governance— sends the artifact to the synodic gate for governance review.manual-approval— parks for a human click in the dashboard.
The order matters: stage 0 runs first, then 1, then 2. A stage that doesn't pass parks the artifact at that stage with a workflow_parked_reason; the workflow only releases when every stage passes (artifact state → released).
Step 4 — call propose_workflow
{
"workspace_id": "ws_<uuid>",
"name": "Triage every labeled issue",
"trigger": { "kind": "github_issue_webhook", "repo": "acme/widgets", "label": "needs-triage" },
"install_id": 12345,
"stages": [
{ "gate_kind": "agent-session", "params": { "prompt": "Classify this issue and add labels." } },
{ "gate_kind": "governance" }
]
}
Important: do not pass active: true. The MCP entry point doesn't plumb the request headers the activation pipeline needs (it does the GitHub label-create + webhook-register side-effects), so the tool returns InvalidParams if you ask for inline activation. Workflows always land inactive; the user activates them via the dashboard or REST PATCH.
After the call succeeds, tell the user: "Created workflow <name> (id <wf_…>), currently inactive. Activate it in the dashboard at Workflows → → Activate." Don't try to activate it through MCP.
Step 5 — schedule changes
To change an existing workflow's trigger (e.g. swap cron expressions, or move from manual to cron), use schedule_workflow:
{
"workflow_id": "wf_…",
"trigger": { "kind": "cron", "expression": "0 9 * * *", "timezone": "UTC" }
}
schedule_workflow replaces the trigger atomically. It validates the kind against the registry manifest and rejects the self-amplifying spine_event { event_kind: "trigger.fired" } case for the same reason propose_workflow does.
Step 6 — deactivate
edit_workflow can deactivate (active: false). Re-activation is not supported via MCP today — same headers issue as propose_workflow. If the user asks to re-activate, point them at the dashboard or REST PATCH.
{ "workflow_id": "wf_…", "active": false }
Common shapes (copy-paste templates)
"Run a stage on every merged PR"
{
"workspace_id": "<ws>",
"name": "Merged-PR review pass",
"trigger": {
"kind": "github_pull_request_closed",
"repo": "acme/widgets",
"predicate": { "merged": true }
},
"install_id": <install_id>,
"stages": [
{ "gate_kind": "agent-session", "params": { "prompt": "Review this PR for spec drift." } }
]
}
"Run something every morning"
{
"workspace_id": "<ws>",
"name": "Morning digest",
"trigger": { "kind": "cron", "expression": "0 9 * * *", "timezone": "UTC" },
"install_id": 0,
"stages": [
{ "gate_kind": "agent-session", "params": { "prompt": "Summarise yesterday's merged PRs." } }
]
}
"A one-shot workflow I'll fire by hand"
{
"workspace_id": "<ws>",
"name": "Ad-hoc backfill",
"trigger": { "kind": "manual", "name": "go" },
"install_id": 0,
"stages": [
{ "gate_kind": "agent-session", "params": { "prompt": "Backfill missing labels on open issues." } }
]
}
"Run on a spine event"
{
"workspace_id": "<ws>",
"name": "On artifact registration",
"trigger": {
"kind": "spine_event",
"event_kind": "artifact.registered",
"filter": { "equals": { "$.payload.kind": "pull_request" } }
},
"install_id": 0,
"stages": [
{ "gate_kind": "agent-session", "params": { "prompt": "Welcome the artifact author." } }
]
}
Failure modes to watch for
InvalidParams: MCP propose_workflow cannot activate inline …— you passedactive: true. Drop it and tell the user to activate from the dashboard.InvalidParams: trigger kind…is not in the registry manifest— typo in the trigger kind, or the kind hasn't shipped yet. Fall back tomanualwhile the user files a spec for the new kind.InvalidParams: spine_event workflow cannot listen fortrigger.fired…— the requested workflow would self-amplify. Suggest a different event kind (e.g.artifact.registered).- Schema validation error on
gate_kind—gate_kindis kebab-case (agent-session, notagent_session). Same fortrigger.kind— snake_case (github_issue_webhook, notgithubIssueWebhook). Unauthorized— the PAT doesn't have access to the workspace the user named. Have them re-issue the PAT scoped to that workspace.
Related skills
onsager-run-workflow— fire a workflow once it's active.onsager-explore-artifacts— inspect what a run produced.onsager-triage-run— diagnose a run that failed or got stuck.