Platform Foundationsv0.2.0
AI

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-task

Recipes 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

WorkflowLifecycle labelsEntryIn-progressExit
Devcode:pick, code:doing, code:done, code:reviewcode:pickcode:doingcode:done or code:review
Refinerefine:pick, refine:doing, refine:questions, refine:answers, refine:redoing, refine:donerefine:pickrefine:doingrefine:done or refine:questions
Auditaudit:report, audit:doing, audit:closed(auto-generated)audit:doingaudit:closed
Routingpriority, bugAdded to an entry labelN/AChanges 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.

  1. priority + bug + the loop entry label
  2. priority + the loop entry label
  3. bug + the loop entry label
  4. 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 + bug work first, then priority, then bug, then normal code:pick work. Each tier picks its lowest-numbered issue.
  • {{title}}, {{body}}, {{number}} flow from dev-pick into the AI implement task and the commit message (fix: resolve #{{number}} {{title}}).
  • {{prUrl}} is emitted by dev-pr as JSON and threaded through CI wait, CI fix, merge, and failure recovery tasks.
  • maxRuns: 5 on verify, maxRuns: 3 on CI wait — bounded retries before the issue is relabeled code:review for a human.
  • Recovery tasks reset the git tree (git reset --hard, git clean -fd, git switch main) and relabel the issue before halting.
  • exit 75 on dev-pick signals "no issues to pick" and routes to the idle dev-nothing task.
  • /repo-verify and /plan-goal from 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 output

Context 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

PatternMeaning
exit 75"No work to do" — routes to the idle *-nothing task
silentChain: trueIdle/no-op task, suppressed in board output
maxRuns: NRetry cap: stops after N executions of that task
*.preflightClean tree + sync main before any real work
*.nothingIdle terminal task (echo, silentChain)
*.failRecovery 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 classDef nodes: start (white), action (purple), decision (amber), idle (dark), failure (red), success (green)
  • ↻N suffix on task names indicates maxRuns