> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polpo.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Loops

> Project-level Agentic Loop API

Agentic Loops are reusable deterministic graphs stored at project level. Create a loop once with `/v1/loops`, then assign it to one or more agents with `assignedLoops` on `/v1/agents`.

Loops are not Missions. Missions orchestrate project work and tasks; loops define how an agent routes its own runtime behavior through deterministic steps.

The runtime contract is `ProjectLoopConfig` JSON. You can also author a loop as static TypeScript DSL source (`export default defineLoop({...})`) and pass it as `{ "source": "..." }`; Polpo compiles it to the same JSON before validation and storage. The compiler recognizes the DSL helpers statically. The DSL is an authoring layer, not arbitrary executable orchestration code.

Use `type: "tool"` for deterministic sandbox/tool actions with no model turn. Use `toolChoice` on `type: "agent"` when the model should still reason but must call a tool. Loop JSON must not contain secrets; custom tools read external credentials from project Connections through `ctx.connections`.

At runtime, Polpo executes the selected project loop as a shared context graph. A `tool` step writes its result into the context bag at `saveAs` (or the tool name when `saveAs` is omitted), and each later `agent` step receives that context as runtime data in its system prompt. `saveAs` does not create shell variables; for example `saveAs: "timing.start"` creates `context.timing.start`, which later guards and agent steps can read.

When a chat completion or task specifies `loop`, the loop must be assigned to the target agent with `assignedLoops`. Loop selection is explicit; omitting `loop` uses the normal runtime behavior.

Loops can also carry enterprise controls:

* `permissions` are readable allow, deny, or approval gates for resources such as `tool`, `step`, `model`, `human`, and `loop`.
* `policies` are expression-based compliance gates for rules that depend on runtime context or payload.
* `hooks` run deterministic tool actions at lifecycle points such as `loop:start`, `tool:before`, `tool:after`, and `loop:end`.

Every run persists a trace. The dashboard uses that trace for Loop Run Audit, Permission Audit, Hook Audit, and the consolidated Loop Governance page.

Every tool referenced by a `tool` step, a lifecycle hook, or an `agent` step `toolChoice` must exist in the project tool catalog and be enabled for the target agent.

## List loops

<ParamField method="GET" path="/v1/loops" />

Returns all project-level loop definitions.

## Get loop

<ParamField method="GET" path="/v1/loops/{name}" />

<ParamField path="name" type="string" required>
  Loop name.
</ParamField>

## Create or replace loop

<ParamField method="POST" path="/v1/loops" />

The request body can be either the canonical loop JSON or `{ "source": "...TypeScript DSL..." }`.

```bash curl theme={null}
curl -X POST https://{project}.polpo.cloud/v1/loops \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "qa-flow",
    "context": "shared",
    "start": "clone_repo",
    "steps": {
      "clone_repo": {
        "type": "tool",
        "tool": "clone_repository",
        "input": {
          "repoUrl": "https://github.com/acme/app.git",
          "targetDir": "workspace/app"
        },
        "saveAs": "repo.clone",
        "next": "plan"
      },
      "plan": {
        "type": "agent",
        "systemPrompt": "Create a concise test plan.",
        "tools": ["read", "grep"],
        "next": "approve_plan"
      },
      "approve_plan": {
        "type": "human",
        "notify": ["ops"],
        "output": { "schema": { "decision": "string" } },
        "next": [
          { "when": "approve_plan.decision == '\''approve'\''", "to": "verify" },
          { "to": "end" }
        ]
      },
      "verify": {
        "type": "agent",
        "systemPrompt": "Run tests and summarize failures.",
        "tools": ["read", "bash"],
        "toolChoice": { "mode": "required", "tool": "bash" },
        "next": "end"
      }
    },
    "permissions": [
      {
        "id": "approve-bash",
        "resource": "tool",
        "effect": "approval",
        "match": { "tool": "bash", "hook": "tool:before" },
        "message": "Approve shell execution before verification."
      }
    ],
    "hooks": {
      "loop:start": [
        {
          "tool": "audit_event",
          "input": { "event": "qa-flow-started" },
          "saveAs": "audit.started"
        }
      ],
      "loop:end": [
        {
          "tool": "audit_event",
          "input": { "event": "qa-flow-ended" },
          "saveAs": "audit.ended",
          "onError": "continue"
        }
      ]
    }
  }'
```

TypeScript DSL authoring example:

```bash curl theme={null}
curl -X POST https://{project}.polpo.cloud/v1/loops \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "source": "export default defineLoop({\n  name: 'qa-flow',\n  start: 'plan',\n  steps: {\n    plan: agentStep({\n      label: 'Plan',\n      systemPrompt: 'Create a concise test plan.',\n      tools: ['read', 'grep'],\n      next: 'verify'\n    }),\n    verify: agentStep({\n      label: 'Verify',\n      systemPrompt: 'Run verification and summarize failures.',\n      tools: ['read', 'bash'],\n      toolChoice: requireTool('bash'),\n      next: [when('verify.done == true', 'end'), otherwise('end')]\n    })\n  }\n});"
}
JSON
```

## Replace loop

<ParamField method="PUT" path="/v1/loops/{name}" />

Replaces the full loop definition. The request body uses the same shape as `POST /v1/loops`: canonical JSON or `{ "source": "...TypeScript DSL..." }`.

## Delete loop

<ParamField method="DELETE" path="/v1/loops/{name}" />

Deletes the project-level loop definition. Agents that still reference the loop should be updated with `/v1/agents/{name}`.

## Run audit

<ParamField method="GET" path="/v1/loop-runs" />

Lists recent loop runs. Filter with `loopName`, `agentName`, `sessionId`, `user`, `status`, and `limit`.

<ParamField method="GET" path="/v1/loop-runs/{id}" />

Returns one persisted loop run including status, context, approval state, and trace events.

<ParamField method="POST" path="/v1/loop-runs/{id}/approve" />

Approves a loop run that is waiting on an approval gate and resumes execution from the checkpoint.

<ParamField method="POST" path="/v1/loop-runs/{id}/reject" />

Rejects a loop run that is waiting on an approval gate.

## Permission audit

<ParamField method="GET" path="/v1/permission-decisions" />

Lists runtime permission, policy, and approval decisions extracted from loop traces. Filter with `loopName`, `agentName`, `sessionId`, `user`, `kind`, `outcome`, `resource`, `hook`, `runStatus`, and `limit`.

Use this endpoint to answer why a tool, model, step, human gate, or loop transition was allowed, denied, or sent to approval.

## Hook audit

<ParamField method="GET" path="/v1/hook-executions" />

Lists deterministic hook tool calls and results extracted from loop traces. Filter with `loopName`, `agentName`, `sessionId`, `user`, `hook`, `tool`, `phase`, `outcome`, `runStatus`, and `limit`.

Use this endpoint to inspect setup/finalization hooks, before/after tool hooks, audit hooks, and failed hook executions.

## Dashboard governance

Loop Studio includes dedicated audit surfaces:

* **Run detail** shows the persisted trace, context bag, approval metadata, and event payloads for one loop run.
* **Permissions** shows permission, policy, and approval decisions across runs.
* **Hooks** shows lifecycle hook executions across runs.
* **Governance** aggregates recent runs, controls, hook evidence, coverage, and attention signals for GRC review.

## Minimal timing loop

```json theme={null}
{
  "name": "time-tracker",
  "context": "shared",
  "start": "capture_start",
  "steps": {
    "capture_start": {
      "type": "tool",
      "tool": "unix_time",
      "saveAs": "timing.start",
      "next": "capture_end"
    },
    "capture_end": {
      "type": "tool",
      "tool": "unix_time",
      "saveAs": "timing.end",
      "next": "summarize"
    },
    "summarize": {
      "type": "agent",
      "systemPrompt": "Read timing.start and timing.end from the loop runtime context. Compute duration = timing.end - timing.start and answer in seconds.",
      "next": "end"
    }
  }
}
```
