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

# Render Templates for Custom Data Views

Render templates show trace data through a small JSX component that receives the data as `data` and renders it however you want. Describe the view you want and Laminar generates the component for you, or write the JSX by hand. Use them when the default views don't show your data the way you want to read it.

Templates come in two scopes:

* **Span templates** render one pane's value: a span's input or output. Use them to reshape a single payload, like a list of flights or a tool result.
* **[Trace templates](#trace-templates)** render a whole trace. They receive every span matching a SQL filter you define, so they can combine data across spans: an executor's output next to an evaluator's target, an agent's plan next to its final answer.

## Where to find them

Every input/output viewer in Laminar has a mode picker in the top-left corner. Alongside the default **LLM Messages**, **JSON**, **YAML**, and **TEXT** modes, the picker lists every render template in the project under **Custom**. Pick one and the pane renders through that template.

<Frame caption="The mode picker on a span's output, with the default modes on top and the project's render templates under Custom.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/render-templates/mode-picker-dropdown.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=6db859a95dfd4696e32dc80bc84d6daa" alt="Mode picker dropdown showing Custom group with a Flights list template" width="1512" height="982" data-path="images/render-templates/mode-picker-dropdown.png" />
</Frame>

<br />

<Frame caption="Same span, rendered through a Flights table template instead of raw JSON. The Edit template button next to the picker opens the editor without leaving the trace.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/render-templates/custom-mode-rendered.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=8b80f5e2b2c8735da17cfb3634d21d43" alt="Span Output rendered through a custom Flights table template" width="1512" height="982" data-path="images/render-templates/custom-mode-rendered.png" />
</Frame>

## Generate a template

Open any I/O view, pick the mode dropdown, and click **+ New template**. The dialog opens with the pane's payload already loaded under the **Sample data** tab, so Laminar knows the shape of your data before you type anything. Then:

1. Name the template.
2. Describe the view you want in the text field, like "a table of flights with price, airline, and departure time, cheapest first".
3. Click **Generate**. Laminar writes the JSX against your sample data and the **Preview** tab renders it live.
4. Not quite right? Describe the change and click **Request changes**. Each iteration edits the existing template instead of starting over.
5. Click **Save**.

<Frame caption="The template editor: name and description on the left, Preview / Sample data / Code tabs on the right. Generate writes the JSX and the preview renders it against the pane's real payload.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/render-templates/edit-dialog.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=c2cc291316f8f0a26b4a7ff755189c1a" alt="Render template dialog with a describe field, Generate button, and live preview" width="1512" height="982" data-path="images/render-templates/edit-dialog.png" />
</Frame>

The generated code follows Laminar's style guide (theme tokens, Tailwind classes), so templates match the rest of the UI without extra prompting.

You can also write or edit the JSX by hand in the **Code** tab. The same tab has a **Copy prompt** button that packages the style guide and your sample data into a prompt for an external AI tool, if you'd rather generate the code elsewhere and paste it back.

## Manage templates

Open **Project Settings → Render Templates** to edit or delete any template in the project, span-scoped and trace-scoped alike.

## What a span template looks like

A template is one function that takes `{ data }` and returns JSX:

```jsx theme={null}
function({ data }) {
  return (
    <div className="w-full min-h-full p-4 text-sm text-foreground bg-background">
      {Array.isArray(data?.messages)
        ? data.messages.map((m, i) => (
            <div key={i} className="mb-2">
              <span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
                {m.role}
              </span>
              <div className="text-sm">{m.content}</div>
            </div>
          ))
        : <div className="text-muted-foreground italic">No messages</div>}
    </div>
  );
}
```

`data` can be anything the pane shows: an object, array, primitive, `null`, or `undefined`.

## Trace templates

A trace template renders the whole trace instead of one span's pane. It's the same JSX function, but `data` is a set of spans matched by a SQL filter you define, so one view can combine an executor's output with an evaluator's target, or show only the tool calls out of a long agent run.

### Create one

Open any trace and click the view dropdown in the trace pane (where **Tree** and **Transcript** live). Your trace templates are listed under **Custom**; **+ New template** opens the editor.

<Frame caption="The trace view dropdown: Tree and Transcript on top, the project's trace templates under Custom, and + New template at the bottom.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/render-templates/trace-view-dropdown.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=9c6feaa5404c9ddce6e2a09922df0067" alt="Trace view dropdown showing default Tree and Transcript modes and custom trace templates" width="1512" height="982" data-path="images/render-templates/trace-view-dropdown.png" />
</Frame>

The flow is the same describe-and-generate as for span templates, with one addition: a trace template has a **Span filter (SQL WHERE)** that picks which spans it receives. Whatever the filter says is appended to `SELECT * FROM spans WHERE trace_id = <trace> AND (...)`; empty means every span. When you click **Generate**, Laminar reads an outline of the trace you opened the editor from and writes both the filter and the JSX to match your description: ask for "executor output diffed against the evaluator's target" and the filter comes back matching only those span types.

The filter lives under the **Sample data** tab. Edit it by hand there if you want, and click **Test** to run it against the current trace and reload the sample data and preview with the real result. After each generation, the sample data refreshes automatically against the new filter.

<Frame caption="The trace template editor after generating: the description produced both the span filter and the JSX, and the preview renders the SQL diff against the real trace.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/render-templates/trace-template-dialog.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=7e7b79488aa51a8581e4e5ba4e63ceba" alt="Render template dialog with a describe field, Generate button, and live preview of a SQL diff view" width="1512" height="982" data-path="images/render-templates/trace-template-dialog.png" />
</Frame>

<br />

<Frame caption="The Sample data tab of the same template: the generated span filter on top, the Test button to rerun it, and the real spans it returned below.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/render-templates/trace-template-sample-data.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=88caf8a53f9aa208228d4d778c493fe7" alt="Sample data tab showing the generated SQL span filter, a Test button, and the JSON spans payload" width="1512" height="982" data-path="images/render-templates/trace-template-sample-data.png" />
</Frame>

The pane remembers which view you picked. Once you switch a trace to a template, every trace you open next renders through it until you switch back, which makes paging through eval datapoints or similar traces fast.

### What the template receives

`data` is an object with two fields:

```typescript theme={null}
{
  spans: Array<{
    spanId: string;
    parentSpanId: string | null;
    name: string;
    path: string;
    spanType: string;   // 'LLM', 'TOOL', 'EXECUTOR', 'EVALUATOR', ...
    startTime: string;
    endTime: string;
    status: string;
    model: string;
    input: unknown;     // JSON-parsed when possible, otherwise raw string
    output: unknown;
    attributes: unknown;
  }>;
  truncated: boolean;   // true if the trace had more than 256 matching spans
}
```

Spans arrive ordered by start time. At most 256 spans are passed to the template; if the filter matches more, `truncated` is `true`, so render a notice when you expect long traces.

### Example: diff an eval output against its target

The evaluation below generates SQL from analytics questions and scores it against a target query with `exact_match` and `token_f1` evaluators:

```typescript text2sql-eval.ts theme={null}
import { evaluate, LaminarAiSdkTelemetry } from '@lmnr-ai/lmnr';
import { registerTelemetry, generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

registerTelemetry(new LaminarAiSdkTelemetry());

const SCHEMA = `
customers(id, name, email, created_at)
orders(id, customer_id, total, status, created_at)
order_items(id, order_id, product_id, quantity, price)
products(id, name, category, price)
`.trim();

const generateSql = async (data: { question: string }) => {
  const response = await generateText({
    model: openai('gpt-5-mini'),
    system: `You translate analytics questions into SQL for this schema:\n${SCHEMA}\nReturn ONLY the SQL query.`,
    prompt: data.question,
  });
  return response.text.trim();
};

const normalize = (sql: string) => sql.toLowerCase().replace(/\s+/g, ' ').replace(/;\s*$/, '').trim();
const tokens = (sql: string) => normalize(sql).split(/[\s,()]+/).filter(Boolean);

const exactMatch = (output: string, target: string) =>
  normalize(output) === normalize(target) ? 1 : 0;

const tokenF1 = (output: string, target: string) => {
  const out = tokens(output);
  const tgt = tokens(target);
  const tgtCounts = new Map<string, number>();
  for (const t of tgt) tgtCounts.set(t, (tgtCounts.get(t) ?? 0) + 1);
  let overlap = 0;
  for (const t of out) {
    const c = tgtCounts.get(t) ?? 0;
    if (c > 0) {
      overlap += 1;
      tgtCounts.set(t, c - 1);
    }
  }
  if (out.length === 0 || tgt.length === 0) return 0;
  const precision = overlap / out.length;
  const recall = overlap / tgt.length;
  return precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);
};

evaluate({
  data: [
    {
      data: { question: 'What is the total revenue from completed orders?' },
      target: "SELECT SUM(o.total) AS total_revenue FROM orders o WHERE o.status = 'completed';",
    },
    {
      data: { question: 'Which products have never been ordered?' },
      target: 'SELECT p.name FROM products p LEFT JOIN order_items oi ON oi.product_id = p.id WHERE oi.id IS NULL;',
    },
    // ... more datapoints
  ],
  executor: generateSql,
  evaluators: { exact_match: exactMatch, token_f1: tokenF1 },
  name: 'text-to-SQL v1',
  groupName: 'text-to-sql',
});
```

`token_f1` tells you *how far* the output is from the target, but not *where* it differs. A trace template can show that: on an eval trace, the executor span's input holds the question and its output holds the generated SQL, and each evaluator span's input is `[output, target]`, so the target is right there in the trace. A description like "diff the executor's SQL output against the evaluator's target, word by word, with the scores on top" generates a template like the one below.

Span filter:

```sql theme={null}
span_type IN ('EXECUTOR', 'EVALUATOR')
```

Template:

```jsx theme={null}
function({ data }) {
  const spans = data?.spans ?? [];
  const executor = spans.find((s) => s.spanType === "EXECUTOR");
  const evaluators = spans.filter((s) => s.spanType === "EVALUATOR");

  const question = executor?.input?.question ?? "";
  const output = typeof executor?.output === "string" ? executor.output : "";
  const evalWithTarget = evaluators.find((s) => Array.isArray(s.input) && s.input.length > 1);
  const target = evalWithTarget ? String(evalWithTarget.input[1] ?? "") : "";

  const tokenize = (sql) => sql.split(/(\s+)/).filter((t) => t.length > 0);

  // Word-level LCS diff between output and target
  const diff = (a, b) => {
    const n = a.length, m = b.length;
    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
    for (let i = n - 1; i >= 0; i--) {
      for (let j = m - 1; j >= 0; j--) {
        dp[i][j] = a[i].trim().toLowerCase() === b[j].trim().toLowerCase()
          ? dp[i + 1][j + 1] + 1
          : Math.max(dp[i + 1][j], dp[i][j + 1]);
      }
    }
    const left = [], right = [];
    let i = 0, j = 0;
    while (i < n && j < m) {
      if (a[i].trim().toLowerCase() === b[j].trim().toLowerCase()) {
        left.push({ t: a[i], k: "same" });
        right.push({ t: b[j], k: "same" });
        i++; j++;
      } else if (dp[i + 1][j] >= dp[i][j + 1]) {
        left.push({ t: a[i], k: "del" });
        i++;
      } else {
        right.push({ t: b[j], k: "add" });
        j++;
      }
    }
    while (i < n) { left.push({ t: a[i], k: "del" }); i++; }
    while (j < m) { right.push({ t: b[j], k: "add" }); j++; }
    return { left, right };
  };

  const { left, right } = diff(tokenize(output), tokenize(target));

  const Chunk = ({ part }) => {
    if (part.k === "same") return <span>{part.t}</span>;
    if (part.k === "del")
      return <span className="bg-destructive/20 text-destructive rounded-sm">{part.t}</span>;
    return <span className="bg-primary/20 text-primary rounded-sm">{part.t}</span>;
  };

  return (
    <div className="w-full min-h-full p-4 space-y-4 text-sm text-foreground bg-background">
      <div>
        <div className="text-xs font-medium uppercase tracking-wide text-muted-foreground mb-1">
          Question
        </div>
        <div className="text-base">{question}</div>
      </div>

      <div className="flex gap-2">
        {evaluators.map((s) => (
          <div key={s.spanId} className="rounded-md border border-border px-2 py-1 text-xs">
            <span className="text-muted-foreground mr-1">{s.name}</span>
            <span className="font-medium">{String(s.output)}</span>
          </div>
        ))}
      </div>

      <div className="grid grid-cols-2 gap-3">
        <div className="rounded-md border border-border">
          <div className="border-b border-border px-3 py-1.5 text-xs font-medium text-muted-foreground">
            Model output
          </div>
          <pre className="p-3 text-xs whitespace-pre-wrap font-mono">
            {left.map((part, idx) => <Chunk key={idx} part={part} />)}
          </pre>
        </div>
        <div className="rounded-md border border-border">
          <div className="border-b border-border px-3 py-1.5 text-xs font-medium text-muted-foreground">
            Target
          </div>
          <pre className="p-3 text-xs whitespace-pre-wrap font-mono">
            {right.map((part, idx) => <Chunk key={idx} part={part} />)}
          </pre>
        </div>
      </div>

      {data?.truncated && (
        <div className="text-xs text-destructive">Span list truncated.</div>
      )}
    </div>
  );
}
```

Open any datapoint of the eval and pick the template from the view dropdown:

<Frame caption="An eval trace rendered through the template: the question, both scores, and a word diff between the generated SQL and the target.">
  <img src="https://mintcdn.com/laminarai/XAojCVbhs9bn1a75/images/evaluations/eval-custom-renderer.png?fit=max&auto=format&n=XAojCVbhs9bn1a75&q=85&s=995ff2aad869c57f76fae6a42f21d5f0" alt="Evaluation trace rendered through a custom SQL diff template showing model output and target side by side" width="1512" height="982" data-path="images/evaluations/eval-custom-renderer.png" />
</Frame>

Clicking through datapoints keeps the view, so reviewing a whole run takes seconds per row. See [Custom rendering](/evaluations/custom-rendering) for the eval-focused walkthrough and [Compare runs](/evaluations/comparing-runs#see-why-a-score-moved-with-a-render-template) for how this fits into a regression workflow.

## Next steps

<CardGroup cols={2}>
  <Card title="Viewing traces" icon="eye" href="/platform/viewing-traces">
    Transcript view, tree view, timeline, and the I/O panes where render templates apply.
  </Card>

  <Card title="Compare runs" icon="chart-line" href="/evaluations/comparing-runs">
    Progression charts, side-by-side deltas, and trace templates in the eval review loop.
  </Card>

  <Card title="Query across traces" icon="database" href="/platform/sql-editor">
    Use the SQL editor, SQL API, [CLI](/platform/cli), or [MCP server](/platform/mcp) to slice every span and signal across your traces.
  </Card>
</CardGroup>
