Loop recipes
Production-grade loop-task recipes with Mermaid diagrams. From simple verify-and-fix patterns to full delivery pipelines that pick, implement, verify, ship, and close issues autonomously.
Check the recommendation first
Recipes are for loop-task, which is the fallback. If the work reacts to a repository event, the Platform default is Agentic Workflows on a self-hosted runner, and these patterns translate to event triggers rather than intervals.
Recipes are the fastest way to learn loop-task. Each recipe is a YAML file with a task chain, a cadence, and an embedded Mermaid diagram that documents the flow. Install the loop-task skills so your agent understands the recipe schema:
npx skills add plainconceptsplatform/loop-taskRecipes live in .loops/recipes/ inside a repo. Drop a YAML file there and loop-task picks it up.
The label system
The production recipes below use GitHub issue labels as a state machine. Lifecycle labels mark where an issue is in a loop, while routing labels determine what the loop picks next. This keeps multiple loops running on the same repo without colliding.
Labels by workflow
| Workflow | Lifecycle labels | Entry | In-progress | Exit |
|---|---|---|---|---|
| Dev | code:pick, code:doing, code:done, code:review | code:pick | code:doing | code:done or code:review |
| Refine | refine:pick, refine:doing, refine:questions, refine:answers, refine:redoing, refine:done | refine:pick | refine:doing | refine:done or refine:questions |
| Audit | audit:report, audit:doing, audit:closed | (auto-generated) | audit:doing | audit:closed |
| Routing | priority, bug | Added to an entry label | N/A | Changes pick order, not lifecycle |
Priority cascade
The Dev, Refine, and Re-refine loops all select the lowest-numbered eligible issue from the
first non-empty tier below. priority is an explicit escalation; bug raises defects ahead of
normal planned work.
priority+bug+ the loop entry labelpriority+ the loop entry labelbug+ the loop entry label- The loop entry label only
Each loop uses its own entry label: code:pick, refine:pick, or refine:answers.
pick_number() {
gh issue list --label "$1" --state open --limit 1000 --json number \
--jq 'sort_by(.number) | .[0].number'
}
number=$(pick_number "priority,bug,$entry")
test -n "$number" || number=$(pick_number "priority,$entry")
test -n "$number" || number=$(pick_number "bug,$entry")
test -n "$number" || number=$(pick_number "$entry")Label lifecycle
The two refinement loops share a label state machine. An issue moves through it as the product owner and the AI agent trade turns:
stateDiagram-v2 [*] --> refine_pick refine_pick --> refine_doing: loop picks refine_doing --> refine_done: story complete refine_doing --> refine_questions: agent has questions refine_questions --> refine_answers: owner answers refine_answers --> refine_redoing: re-refine loop picks refine_redoing --> refine_done: story complete refine_redoing --> refine_questions: more questions refine_done --> [*]
The dev loop is a simpler linear flow:
stateDiagram-v2 [*] --> code_pick code_pick --> code_doing: loop picks code_doing --> code_done: implementation complete code_doing --> code_review: needs human review code_review --> code_pick: human re-queues code_done --> [*]
Built-in recipes
These ship with the loop-task repo as simple, self-contained examples.
Issue count (silent check)
Fetch issue count every 10 seconds. On failure (e.g. gh not authenticated), fall through to a silent idle task instead of crashing the loop.
flowchart TD
fetchStart("Start<br/>Fetch issue count") --> fetchIssues
fetchIssues["Fetch issues<br/>gh issue list + count"] -->|✓| report
fetchIssues -.->|✗| logSilentError
report(("Complete<br/>Echo total issue count"))
logSilentError(("Fail<br/>Silent chain fallback"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class fetchStart start
class fetchIssues decision
class report success
class logSilentError idle
Key pattern: silentChain: true on the fallback task so the board does not show a noisy failure every 10 seconds when the CLI is unavailable.
Oldest issue
List all repo issues, print the oldest one using {{key}} template interpolation.
flowchart TD
listStart("Start<br/>List all repo issues") --> listIssues
listIssues["List issues<br/>Fetch open+closed via gh"] -->|✓| printOldest
listIssues -.->|✗| listFail
printOldest("Print oldest<br/>Echo #number + title + date")
listFail(("Fail<br/>gh not authenticated or error"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class listStart start
class printOldest action
class listIssues decision
class listFail failure
Key pattern: gh issue list --jq '{oldestNumber: ...}' emits a JSON object, and the next task interpolates {{oldestNumber}}, {{oldestTitle}}, {{oldestCreatedAt}}, {{total}} from it.
Verify and fix
The fundamental verify → fix → verify retry loop. Runs lint, typecheck, and tests; on failure an AI agent receives {{output}} and attempts a fix; retries 3 times before giving up.
flowchart TD
verifyStart("Start<br/>Run verify") --> verifyTask
verifyTask["Verify<br/>lint + typecheck + test<br/>↻3"] -->|✓| completeTask
verifyTask -.->|✗| fixTask
fixTask("Fix<br/>AI fix using {{output}}") -->|✓| verifyTask
fixTask -.->|✗| failTask
completeTask(("Complete<br/>All checks passed"))
failTask(("Fail<br/>Verification failed after retries"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class verifyStart start
class fixTask action
class verifyTask decision
class failTask failure
class completeTask success
Key pattern: maxRuns: 3 on the verify task caps retries. The fix task receives {{output}} (the full lint/test errors) and loops back to verify.
Production recipes
These power the Numa project. They are the best reference for building real loop-engineering pipelines: multi-task chains with recovery, retry caps, AI agents, and GitHub label state machines.
Dev loop
The flagship. Every 20 minutes, picks a code:pick issue through the priority cascade, implements it end-to-end with an AI agent, verifies (lint + typecheck + tests + build for both frontend and backend), opens a PR, waits for CI, fixes CI failures, merges, and closes the issue. 16 tasks cover the full delivery lifecycle.
flowchart TD
devStart("Start<br/>Begin development cycle") --> devPreflight
devPreflight["Preflight<br/>Clean tree + sync main"] -->|✓| devPick
devPreflight -.->|✗| devCleanDirty
devCleanDirty("Recovery<br/>Reset dirty tree + resync (maxRuns=1)") -->|✓| devPick
devPick["Select<br/>Priority cascade pick"] -->|✓| devImplement
devPick -.->|✗| devNothing
devImplement("Implement<br/>AI: opencode /plan-goal") -->|✓| devVerify
devImplement -.->|✗| devFail
devVerify["Verify<br/>Typecheck + tests + build<br/>↻5"] -->|✓| devCommit
devVerify -.->|✗| devFix
devFix("Fix<br/>Resolve verification failures") -->|✓| devVerify
devFix -.->|✗| devFail
devCommit("Commit<br/>Stage and commit changes") -->|✓| devPr
devPr["PR<br/>Push + create pull request"] -->|✓| devAwaitCi
devPr -.->|✗| devFailMerge
devAwaitCi["Check CI<br/>Wait for every required check<br/>↻3"] -->|✓| devCiReady
devAwaitCi -.->|✗| devCiFix
devCiFix("Fix CI<br/>Push fix and post PR note") -->|✓| devAwaitCi
devCiFix -.->|✗| devCiBlocked
devCiReady["Merge PR<br/>Squash and delete branch"] -->|✓| devComplete
devCiReady -.->|✗| devFailMerge
devComplete(("Complete<br/>Close issue + return to main"))
devFailMerge(("Merge Fail<br/>Admin merge or leave for review"))
devFail(("Fail<br/>Reset + relabel for review"))
devCiBlocked(("CI Blocked<br/>Leave PR for human review"))
devNothing(("Idle<br/>No issues to pick"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class devStart start
class devCleanDirty,devImplement,devFix,devCommit,devCiFix action
class devPreflight,devPick,devVerify,devPr,devAwaitCi,devCiReady decision
class devNothing idle
class devFail,devFailMerge,devCiBlocked failure
class devComplete success
Key patterns:
- Priority cascade selects
priority+bugwork first, thenpriority, thenbug, then normalcode:pickwork. Each tier picks its lowest-numbered issue. {{title}},{{body}},{{number}}flow fromdev-pickinto the AI implement task and the commit message (fix: resolve #{{number}} {{title}}).{{prUrl}}is emitted bydev-pras JSON and threaded through CI wait, CI fix, merge, and failure recovery tasks.maxRuns: 5on verify,maxRuns: 3on CI wait — bounded retries before the issue is relabeledcode:reviewfor a human.- Recovery tasks reset the git tree (
git reset --hard,git clean -fd,git switch main) and relabel the issue before halting. exit 75ondev-picksignals "no issues to pick" and routes to the idledev-nothingtask./repo-verifyand/plan-goalfrom opencode-onboard drive the AI implementation and verification phases.
Refine loop
Every 20 minutes, selects a refine:pick issue through the priority cascade and rewrites it as a grounded user story (Mike Cohn format + Gherkin acceptance criteria + Mermaid diagram), running @humanizer on the prose. Ends with refine:done or refine:questions.
flowchart TD
refStart("Start<br/>Begin refinement cycle") --> refPick
refPick["Select<br/>Priority cascade pick"] -->|✓| refRefine
refPick -.->|✗| refNothing
refRefine("Refine<br/>AI: /planStory to rewrite as user story<br/>↻5") -->|✓| refVerify
refRefine -.->|✗| refFail
refVerify["Verify<br/>Issue has refine:questions or refine:done"] -->|✓| refComplete
refVerify -.->|✗| refRefine
refComplete(("Complete<br/>Issue refined successfully"))
refFail(("Fail<br/>Relabel issue back to refine:pick"))
refNothing(("Idle<br/>No issues to refine"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class refStart start
class refRefine action
class refPick,refVerify decision
class refNothing idle
class refFail failure
class refComplete success
Key pattern: verify checks for refine:questions or refine:done labels, looping back to refine on failure (the AI agent is expected to have set one of them). The agent grounds the story in the actual codebase and appends clarifying questions for the product owner if needed.
Re-refine loop
Companion to the refine loop. When a product owner answers clarifying questions (refine:answers), this loop selects an issue through the priority cascade, reads only the issue owner's comments (ignoring others as a security measure), and re-refines the story with the answers.
flowchart TD
rerefStart("Start<br/>Begin re-refinement cycle") --> rerefPick
rerefPick["Select<br/>Priority cascade pick"] -->|✓| rerefProcess
rerefPick -.->|✗| rerefNothing
rerefProcess("Process<br/>AI: /planStory with owner comments<br/>↻5") -->|✓| rerefVerify
rerefProcess -.->|✗| rerefFail
rerefVerify["Verify<br/>Issue has refine:questions or refine:done"] -->|✓| rerefComplete
rerefVerify -.->|✗| rerefProcess
rerefComplete(("Complete<br/>Issue re-refined successfully"))
rerefFail(("Fail<br/>Relabel issue back to refine:answers"))
rerefNothing(("Idle<br/>No issues to re-refine"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class rerefStart start
class rerefProcess action
class rerefPick,rerefVerify decision
class rerefNothing idle
class rerefFail failure
class rerefComplete success
Key pattern: this forms a two-loop cycle with refine-loop. An issue can bounce between refine:questions and refine:answers multiple times as the agent and owner converge on the story.
Guardrails audit loop
Every 12 hours, runs a read-only /repo-audit, publishes the top-3 highest-severity findings as bug + code:pick issues (via /plan-story), and creates an audit:report summary with sub-issue linkage. Verifies at least one new bug issue was created.
flowchart TD
auditStart("Start<br/>Begin guardrails audit cycle") --> auditPreflight
auditPreflight["Preflight<br/>Clean tree + sync main"] -->|✓| auditLock
auditPreflight -.->|✗| auditFail
auditLock["Check lock<br/>No audit already running"] -->|✓| auditAudit
auditLock -.->|✗| auditNothing
auditAudit("AI: /repo-audit<br/>Read-only repository audit<br/>↻3") -->|✓| auditPublish
auditAudit -.->|✗| auditFail
auditPublish("AI: publish findings<br/>Create issues and audit report") -->|✓| auditVerify
auditPublish -.->|✗| auditFail
auditVerify["Verify<br/>At least 1 new bug issue created"] -->|✓| auditComplete
auditVerify -.->|✗| auditAudit
auditComplete(("Complete<br/>Audit finished successfully"))
auditFail(("Fail<br/>Cleanup audit:doing state"))
auditNothing(("Idle<br/>Audit already in progress"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class auditStart start
class auditAudit,auditPublish action
class auditPreflight,auditLock,auditVerify decision
class auditNothing idle
class auditFail failure
class auditComplete success
Key pattern: the audit-lock task prevents overlapping audits by checking for an existing audit:doing issue. If verify fails (no bugs were created), the loop retries the audit itself (back to audit-audit).
Audit cleanup loop
Every day, closes every audit:report issue whose all referenced #NNN bug issues are resolved. Uses an AI agent to extract issue references from report bodies, check their state, and close completed reports.
flowchart TD
cleanupStart("Start<br/>Begin audit cleanup cycle") --> cleanupPreflight
cleanupPreflight("Preflight<br/>Clean tree + sync main") -->|✓| cleanupSelect
cleanupSelect["Select reports<br/>All open audit:report issues"] -->|✓| cleanupAi
cleanupSelect -.->|✗| cleanupNothing
cleanupAi(("Complete<br/>Close resolved audit reports"))
cleanupNothing(("Idle<br/>No open audit reports"))
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
class cleanupStart start
class cleanupPreflight action
class cleanupSelect decision
class cleanupAi success
class cleanupNothing idle
Key pattern: {{output}} carries a JSON array of open audit reports. The AI agent processes each report, extracts #NNN references, checks their state with gh issue view, and closes reports where all referenced issues are resolved.
Recipe anatomy
Every recipe follows the same schema:
version: 2
loops:
- taskId: <entry-task-id> # which task starts the chain
intervalHuman: 20m # cadence (10s, 5m, 1h, 1d, 1w)
description: > # shown in the board
What this loop does
maxRuns: null # optional: stop after N iterations
tasks:
- id: <task-id> # unique identifier
name: "Human-readable name"
command: sh # or: opencode, gh, echo, any CLI
commandArgs: [...] # arguments passed to the command
onSuccessTaskId: <task-id> # chain on exit code 0
onFailureTaskId: <task-id> # chain on non-zero
maxRuns: 5 # optional: retry cap before giving up
silentChain: true # optional: suppress in board outputContext interpolation
Tasks chain context automatically via {{key}} template interpolation. Before a task runs, every {{key}} in its commandArgs is replaced with the current value from the shared context:
- Auto-capture: stdout and stderr from every task are captured before the next task starts.
- JSON: a JSON object on stdout (
{"key": "value"}) merges each key into context. {{output}}: always the immediately preceding task's full stdout + stderr.{{number}},{{title}}, etc.: named keys from a JSON-emitting task persist until overwritten.- Lifecycle: context is fresh per loop iteration, never persisted to disk.
Conventions
| Pattern | Meaning |
|---|---|
exit 75 | "No work to do" — routes to the idle *-nothing task |
silentChain: true | Idle/no-op task, suppressed in board output |
maxRuns: N | Retry cap: stops after N executions of that task |
*.preflight | Clean tree + sync main before any real work |
*.nothing | Idle terminal task (echo, silentChain) |
*.fail | Recovery terminal task (reset git, relabel issue) |
The diagram field
Every recipe includes a diagram: field with an embedded Mermaid flowchart. The convention for the visual language:
-->|✓|solid arrow for on-success transitions-.->|✗|dashed arrow for on-failure transitions- Colored
classDefnodes: start (white), action (purple), decision (amber), idle (dark), failure (red), success (green) ↻Nsuffix on task names indicatesmaxRuns