Skip to main content
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

MethodDescription
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.
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:
EffortUse it for
agenticDefault managed loop execution.
agentic_plusMore 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.