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

# Loops

> Run ontology-guided extraction and hydration loops with pb.loops.

`pb.loops` runs bounded agentic jobs over Penumbra context. A loop reads source
material, works against your shapes, stages graph changes into a delta, and gives
you status, events, and checkpoints while it runs.

Use loops when one extraction call is not enough: long documents, mixed corpora,
relationship-heavy work, or any task that needs several passes over a typed
workspace.

<Info>
  Loops consume Penumbra credits when they execute. Use `dryRun: true` to
  compile and validate the loop request without starting execution.
</Info>

## Create a client

```ts theme={null}
import { createPenumbra } from "@penumbra-systems/platform";

const pb = createPenumbra({ apiKey: process.env.PENUMBRA_API_KEY! });
```

## Methods

| Method                              | Description                                                                        |
| ----------------------------------- | ---------------------------------------------------------------------------------- |
| `pb.loops.run(input)`               | Start a loop. Returns a queued run and links for polling, events, and checkpoints. |
| `pb.loops.get(loopId)`              | Read loop status and the staged result delta.                                      |
| `pb.loops.events(loopId, options?)` | Read ordered loop events.                                                          |
| `pb.loops.checkpoints(loopId)`      | Read iteration checkpoints.                                                        |
| `pb.loops.watch(loopId, options?)`  | Async iterator over new events until the loop reaches a terminal status.           |
| `pb.loops.wait(loopId, options?)`   | Poll until the loop completes, fails, or times out.                                |

## Run a graph extraction loop

Point the loop at source material and the shape or shapes it should extract
into. Loops always stage a delta for review; they do not apply directly.

```ts theme={null}
const run = await pb.loops.run({
  kind: "graph_extraction",
  goal: "Extract customers, initiatives, risks, and relationships from the account memo.",
  sources: ["src_01JZ4..."],
  shape_ids: ["shp_customer_intelligence"],
  loop: {
    effort: "agentic",
    max_iterations: 5,
    quality_threshold: 8,
  },
  output: { type: "delta", apply: false },
});

console.log(run.loop_id, run.status, run.result?.delta_id);
```

The returned `delta_id` is the workspace the loop is building. After the loop
finishes, inspect and apply it through [`pb.deltas`](/sdk/deltas).

```ts theme={null}
if (!run.loop_id) throw new Error("Loop did not return an id");

const status = await pb.loops.get(run.loop_id);

if (status.status === "completed" && status.result?.delta_id) {
  const plan = await pb.deltas.plan(status.result.delta_id);
  // Review the plan, then apply when it is fit to commit.
}
```

## Watch progress

`events` returns an ordered event page. Pass `after_sequence` to continue from the
last event you processed.

```ts theme={null}
let after_sequence: number | undefined;
if (!run.loop_id) throw new Error("Loop did not return an id");

while (true) {
  const page = await pb.loops.events(run.loop_id, {
    after_sequence,
    limit: 50,
  });

  for (const event of page.events) {
    console.log(event);
  }

  after_sequence = page.next_after_sequence ?? after_sequence;

  const current = await pb.loops.get(run.loop_id);
  if (["completed", "failed", "cancelled"].includes(current.status)) break;
}
```

Use checkpoints when you want the iteration-level state rather than the event
feed.

```ts theme={null}
if (!run.loop_id) throw new Error("Loop did not return an id");

const { checkpoints } = await pb.loops.checkpoints(run.loop_id);
```

For most scripts, `watch` and `wait` are the simpler path.

```ts theme={null}
if (!run.loop_id) throw new Error("Loop did not return an id");

for await (const event of pb.loops.watch(run.loop_id)) {
  console.log(event.type, event.sequence_number);
}

const final = await pb.loops.wait(run.loop_id, { timeoutMs: 600_000 });
```

## Effort

`pb.loops` currently supports two effort presets:

| Effort         | Use it for                                                                        |
| -------------- | --------------------------------------------------------------------------------- |
| `agentic`      | Default managed loop execution.                                                   |
| `agentic_plus` | More intensive managed loop execution for harder corpora or stricter review bars. |

## Dry run

Use `dryRun` to validate the request and see the compiled execution plan without
spending credits.

```ts theme={null}
const preview = await pb.loops.run({
  kind: "graph_extraction",
  goal: "Extract deal risks from the memo.",
  sources: ["src_01JZ4..."],
  shape_ids: ["shp_deal_risk"],
  dryRun: true,
});
```

## Hydration loops

`hydrate` is the easier entry point when your goal is to make a subject fit to
act on. Provide a subject and purpose; Penumbra derives the loop goal.

```ts theme={null}
const run = await pb.loops.run({
  kind: "hydrate",
  goal: "Hydrate the Acme account context before drafting the renewal plan.",
  sources: ["src_01JZ4..."],
  shape_ids: ["shp_account_memory"],
  loop: { effort: "agentic", max_iterations: 4 },
});
```

## Required access

Running a loop requires loop write access and shape read access. Reading status,
events, and checkpoints requires loop read access.

For REST details, see [Loops in the API reference](/api-reference/loops).
