> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Traces & spans

> ff.withTrace() groups several LLM calls into one trace so a multi-step operation appears as a single row with its calls attached.

A single logical operation often makes several LLM calls brainstorm then summarize, retrieve then answer. `ff.withTrace()` wraps that work so all the calls inside share one `trace_id` and a parent row lands in `firstflow_traces`.

```ts theme={null}
const result = await ff.withTrace(
  async () => {
    const topic = await ai.messages.create({
      model: "claude-haiku-4-5",
      messages: [{ role: "user", content: "Suggest a haiku topic." }],
      firstflow: { userId, metadata: { step: "brainstorm" } },
    } as any);

    const poem = await ai.messages.create({
      model: "claude-haiku-4-5",
      messages: [{ role: "user", content: `Haiku about: ${topicText}` }],
      firstflow: { userId, metadata: { step: "write" } },
    } as any);

    return poem;
  },
  { name: "haiku_two_step", userId, metadata: { route: "/api/haiku" } },
);
// → one row in firstflow_traces, two in firstflow_llm_calls sharing its trace_id
```

## Options

<ParamField path="name" type="string">
  Human-readable label for the operation, stored on the trace.
</ParamField>

<ParamField path="userId" type="string">
  The user the operation belongs to.
</ParamField>

<ParamField path="metadata" type="Record<string, unknown>">
  Arbitrary tags stored on the trace row.
</ParamField>

<ParamField path="inputState" type="unknown">
  Optional snapshot of the operation's inputs.
</ParamField>

<ParamField path="traceId" type="string">
  Provide an explicit id to correlate with an upstream request; otherwise one is
  generated. `ff.newTraceId()` mints one ahead of time.
</ParamField>

## How it works

`withTrace` uses `AsyncLocalStorage` to make the trace id ambient: any wrapped call inside the callback picks it up automatically you don't thread it through. On completion it records the parent trace (latency = wall-clock of the whole operation); if the callback throws, the trace is recorded with `is_error` and the message, then the error re-throws.

## Querying a trace and its calls

```sql theme={null}
select t.id, t.name, t.latency_ms, count(c.id) as calls,
       sum(c.total_cost_usd) as cost
from firstflow_traces t
left join firstflow_llm_calls c on c.trace_id = t.id
group by t.id;
```

## Next

See the full row shapes in [What gets recorded](/server/observe), or classify the conversation in [Intent & sentiment](/server/analytics).
