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;
});
});
}
# parent.py
import trodo
from worker import process_in_worker
with trodo.wrap_agent("orchestrator") as run:
run.set_input({"task": "summarise docs"})
process_in_worker(run.run_id, doc_id=123) # pass run_id across module boundary
run.set_output({"done": True})
# worker.py
import trodo
def process_in_worker(run_id, doc_id):
with trodo.join_run(run_id):
with trodo.span("fetch-doc", kind="tool") as span:
span.set_tool("fetch-doc")
span.set_input({"docId": doc_id})
doc = db.get(doc_id)
span.set_output({"bytes": len(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 });
});
});
# caller (same process, crossing to Python service)
with trodo.wrap_agent("parent-service"):
requests.post(
"http://child-service/work",
headers=trodo.propagation_headers(),
json={"task": "summarise"},
)
# child service (FastAPI)
from fastapi import FastAPI
import trodo
app = FastAPI()
app.middleware("http")(trodo.fastapi_middleware()) # joins the caller's run
@app.post("/work")
def work(body: dict):
with trodo.span("summarise", kind="tool") as span:
span.set_tool("summarise")
span.set_input(body)
summary = summarise(body["task"])
span.set_output({"summary": summary})
return {"summary": summary}
See Recipes → Cross-service for the full runnable example with verification.
joinRun reference
| Signature | What 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.