Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.trodo.ai/docs/llms.txt

Use this file to discover all available pages before exploring further.

What / When / Minimum

Two ways to model “this run triggered another run”:
PatternEffectUse when
joinRun (or cross-service middleware)Downstream work becomes a span inside the parent run. One run, bigger tree.You want a unified waterfall. Both sides are short-lived; both are part of one logical request.
parentRunId on wrapAgentDownstream work is its own run, linked back via parent_run_id. Two runs, cross-referenced.The sub-agent is independently meaningful (long-running, billed separately, re-used by other callers, different agent type).
Rule of thumb: joinRun for microservices serving one request. parentRunId for spawned agents.

parentRunId — separate linked run

const { runId: parentRunId } = await trodo.wrapAgent('planner', async (parent) => {
  const plan = await makePlan(parent.input);

  // Spawn a sub-agent as its own run.
  const { result, runId: childRunId } = await trodo.wrapAgent('executor', async (child) => {
    child.setInput(plan);
    return await execute(plan);
  }, { parentRunId: parent.runId });

  return { plan, result, childRunId };
});
Dashboard shows two rows — planner and executor — each with its own tokens, cost, spans, and feedback. The executor row has a parent link clickable back to planner; the planner detail view lists child runs. Each run is billed as 1 event. Use joinRun instead if you want a single billed event.

joinRun — nested into one run

joinRun is the underlying primitive the cross-service middleware uses. Call it directly when you have a runId and want a callback to run inside it, producing a span instead of a new run:
await trodo.joinRun(
  incomingRunId,
  incomingParentSpanId,
  async (span) => {
    span.setInput(req.body);
    return await handle(req.body);
  },
  { name: 'queue.consume', kind: 'agent' },
);
Typical use: queue consumers (see cross-service → manual joinRun).

Fan-out to many sub-agents

await trodo.wrapAgent('orchestrator', async (parent) => {
  const subtasks = split(parent.input);

  const results = await Promise.all(subtasks.map((t, i) =>
    trodo.wrapAgent(`worker-${i % 3}`, async () => doWork(t), {
      parentRunId: parent.runId,
    }),
  ));

  parent.setOutput({ count: results.length });
});
Dashboard: one orchestrator row + N worker rows, all pointing back via parent_run_id. Click the orchestrator to see the linked children; filter by parent_run_id to find them programmatically.

When tokens/costs roll up

They don’t. Each run’s total_tokens_in/out and total_cost are computed from its own spans only. If you want a “total for this request family”, sum across the parent and its children via the REST API (see rest-api). joinRun, by contrast, rolls everything into the parent — the downstream spans count toward the parent run’s tokens and cost, because they’re spans, not separate runs.

Decision tree

Is the downstream work a *separate agent* — different purpose, reusable, long-lived?
 ├─ yes → wrapAgent(..., { parentRunId })
 └─ no → does it run in the same process?
         ├─ yes → just call the function; AsyncLocalStorage handles it
         └─ no → propagationHeaders + expressMiddleware (or manual joinRun)

Gotchas

  • parentRunId is free-form — you can pass any UUID, even one from a previous session. The backend doesn’t validate existence; if you pass a wrong id, you get a dangling reference.
  • parent_run_id is indexed, so filtering by it in the dashboard is fast.
  • A run can be both a parent (has children) and a child (has parent_run_id) — three-level hierarchies work; UI collapses beyond two but the data is there.