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.
Loops consume Penumbra credits when they execute. Use dryRun: true to
compile and validate the loop request without starting execution.
Create a client
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. |
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.
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.
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.
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.
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.
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.
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.
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.