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.

Inside the wrapAgent callback, every withSpan call attaches to the current run automatically via AsyncLocalStorage (Node) or contextvars (Python). Outside the callback — a different file, a worker, a queue handler, a separate service — that context doesn’t exist. You must pass the runId explicitly and call joinRun. runId is the one compulsory extra parameter. Without it, spans have no run to attach to and are dropped.

Same process, different file

Pass runId as an argument. The receiving function wraps its work in joinRun(runId, fn), and every withSpan inside that wrapper attaches to the parent run.
// parent.js
import trodo from 'trodo-node';
import { processInWorker } from './worker.js';

const { runId } = await trodo.wrapAgent('orchestrator', async (run) => {
  run.setInput({ task: 'summarise docs' });
  await processInWorker(runId, { docId: 123 });   // pass runId across file boundary
  run.setOutput({ done: true });
});
// worker.js
import trodo from 'trodo-node';

export async function processInWorker(runId, { docId }) {
  await trodo.joinRun(runId, async () => {
    await trodo.withSpan({ kind: 'tool', name: 'fetch-doc', toolName: 'fetch-doc' }, async (span) => {
      span.setInput({ docId });
      const doc = await db.get(docId);
      span.setOutput({ bytes: doc.length });
      return doc;
    });
  });
}

Different service

runId travels over an HTTP header. The outbound side attaches it with propagationHeaders(); the inbound side reads it and calls joinRun.
// caller
await trodo.wrapAgent('parent-service', async () => {
  await fetch('http://child-service/work', {
    method: 'POST',
    headers: { ...trodo.propagationHeaders(), 'content-type': 'application/json' },
    body: JSON.stringify({ task: 'summarise' }),
  });
});

// child service (Express)
import express from 'express';
const app = express();
app.use(trodo.expressMiddleware());   // reads X-Trodo-Run-Id, joins automatically

app.post('/work', async (req, res) => {
  await trodo.withSpan({ kind: 'tool', name: 'summarise', toolName: 'summarise' }, async (span) => {
    span.setInput(req.body);
    const summary = await summarise(req.body.task);
    span.setOutput({ summary });
    res.json({ summary });
  });
});
See Recipes → Cross-service for the full runnable example with verification.

joinRun reference

SignatureWhat it does
trodo.joinRun(runId, fn) (Node)Runs fn with runId set as the active run. Every withSpan inside attaches to that run. Flushes spans on return.
trodo.join_run(run_id) (Python, context manager)Same, as a with block.
  • joinRun does not create a new run row. It only lets you emit spans against an existing run.
  • Calling joinRun with a runId that doesn’t exist on your site is a no-op — the spans are dropped silently.
  • If you want a separate run linked to a parent, use wrapAgent with parentRunId instead. See Recipes → Sub-agents.