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

# Outcomes — reporting business results back to a trace

> Use outcome() to attribute a business result — a deflected ticket, an avoided refund, an SLA met — back to the trace, agent, and model that produced it, usually minutes or hours after the trace ended.

An LLM span tells you what a model call cost and how long it took. It doesn't tell you whether the call actually mattered — whether the ticket it answered stayed closed, whether the refund it prevented was real. **Outcomes** close that gap: your own backend reports a business result and attributes it to the trace that produced it, and Zespan joins the two so the [Value](/dashboard/value) dashboard can show cost-per-success and value-per-dollar broken down by agent and model, not just cost-per-call.

<Note>
  **Currently TypeScript-only.** The Python SDK does not yet expose an `outcome()` method. Python customers (and anyone reporting from a non-SDK backend) call `POST /v1/ingest/outcomes` directly — see [Reporting without the TypeScript SDK](#reporting-without-the-typescript-sdk) below. Field names on the wire are the same camelCase (`valueUsd`, `traceId`) either way.
</Note>

## Why out-of-band

Most outcomes aren't known when the trace runs. An agent answers a support message; whether that ticket actually stayed resolved is only known when it doesn't reopen a few hours later, or a refund is only "avoided" once the customer's session ends without one. `outcome()` is built for that gap: it's a direct, one-shot call your backend makes whenever the result becomes known — a webhook handler, a nightly reconciliation job, a support-desk close event — not something you call inside the same request that produced the trace.

Because of that, an explicit `traceId` is the common case, not the exception. `outcome()` also works from inside an active trace (it falls back to the current trace context when `traceId` is omitted), but most real call sites are in a different process, minutes or hours after the trace that they're describing has already finished.

## Reporting an outcome

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { zespan } from "@zespan/sdk";

zespan.init({ apiKey: process.env.ZESPAN_API_KEY! });

await zespan.getClient().outcome({
  kind: "ticket_deflected",
  success: true,
  valueUsd: 12.5,
  traceId: trace.id, // usually known from wherever you stored it when the trace ran
  agentName: "SupportAgent",
});
```

`outcome()` lives on the client, the same place `checkGuardrails()` and `datasets` do — get it from `zespan.getClient()` (or construct/import a `ZespanClient` directly) rather than calling it on the `zespan` object itself.

### `OutcomeInput` fields

<ParamField body="kind" type="string" required>
  A short label for what this outcome measures — `"ticket_deflected"`, `"refund_avoided"`, `"sla_met"`, or any other string your team uses consistently. There's no fixed enum; `kind` is whatever your business tracks, and it's what the Value dashboard groups and filters by. Max 100 characters.
</ParamField>

<ParamField body="success" type="boolean" required>
  Whether this outcome was actually achieved. A deflected-ticket check that failed (the ticket reopened) is still worth reporting — send `success: false` rather than skipping the call — since a `kind`'s success rate is only meaningful if failures are reported too.
</ParamField>

<ParamField body="valueUsd" type="number">
  The dollar value of this outcome, if you can put a number on it (an avoided refund's amount, an estimated support-cost saving). Omit it entirely rather than passing `0` for "unknown" — omitted, it's left out of the request body rather than sent as `null`, and the dashboard's value/cost ratios treat "no value reported" differently from "reported as zero."
</ParamField>

<ParamField body="attributes" type="Record<string, string | number | boolean>">
  Free-form key-value metadata for this outcome. Values are coerced to strings before the request is sent — `attributes: { ticketId: 4821, tier: "gold" }` is transmitted as `{ "ticketId": "4821", "tier": "gold" }` — because every other Zespan SDK surface treats span/event attributes as strings, and this keeps outcomes consistent with that rather than introducing a second, wider attribute type.
</ParamField>

<ParamField body="traceId" type="string">
  The trace this outcome is attributed to. Explicit `traceId` always wins over an active trace, even if `outcome()` happens to be called from inside one — see [Resolving `traceId`](#resolving-traceid) below. Required in practice: `outcome()` throws synchronously if it can't resolve one.
</ParamField>

<ParamField body="sessionId" type="string">
  The session this outcome belongs to, if your traces are grouped into multi-turn sessions. Purely descriptive — not used to resolve `traceId`.
</ParamField>

<ParamField body="agentName" type="string">
  The agent that produced the trace, if known. Populating this is what lets the Value dashboard's **By agent** breakdown attribute the outcome correctly; an outcome reported with no `agentName` still counts toward totals but groups under an empty agent name.
</ParamField>

### Resolving `traceId`

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
// Explicit traceId — the common case, called from an unrelated process
// long after the trace finished.
await zespan.getClient().outcome({
  kind: "refund_avoided",
  success: true,
  traceId: storedTraceId,
});

// No explicit traceId — falls back to the active trace, if outcome() is
// called from inside one.
await zespan.withTrace(async () => {
  // ... your agent runs here ...
  await zespan.getClient().outcome({ kind: "sla_met", success: true });
});
```

An explicit `input.traceId` is always used as-is, never overridden by an active trace even if one exists. When `traceId` is omitted, `outcome()` falls back to the trace currently active in this process (the same context [`withAgent`](/sdk/agent-tracing) and the LLM wrappers read). If neither is available, `outcome()` throws synchronously — it never silently no-ops or sends a request with a missing `traceId`.

<Warning>
  A trace doesn't need to exist in Zespan yet for its outcome to be accepted. Outcomes and traces are joined at query time, not at write time, so an outcome that arrives before its trace has finished ingesting is stored and joined correctly once the trace shows up. This is the normal shape for the out-of-band case, not an edge case to work around.
</Warning>

## Response and errors

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
const result = await zespan.getClient().outcome({
  kind: "ticket_deflected",
  success: true,
  traceId: trace.id,
});
// result: { accepted: number }
```

On success, `outcome()` resolves to `{ accepted: number }` — `1` for the single outcome this call sent. On a non-2xx response from the API, it throws an `Error` whose message includes the HTTP status and, when the server returned one, a truncated response body — for example, a `kind` over the 100-character limit fails with a `400` and the server's validation message.

<Tip>
  A rejected `outcome()` call throws rather than resolving to a failure object, so wrap it in `try`/`catch` (or let it propagate) the same way you would any other awaited network call — there's no `{ ok: false }` shape to check instead.
</Tip>

## Reporting without the TypeScript SDK

Python customers, and anyone reporting from a backend that isn't a Zespan SDK at all, call the ingest endpoint directly. It's the same endpoint `outcome()` wraps — see the full request/response contract in the **Outcomes** group of the [API Reference](/api-reference/introduction).

```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import os
import requests

requests.post(
    "https://api.zespan.com/v1/ingest/outcomes",
    headers={"x-api-key": os.environ["ZESPAN_API_KEY"]},
    json={
        "outcomes": [{
            "kind": "ticket_deflected",
            "success": True,
            "valueUsd": 12.50,
            "traceId": trace_id,
        }]
    },
    timeout=10,
)
```

<Note>
  Field names on the wire are camelCase (`valueUsd`, `traceId`, `sessionId`, `agentName`) even from Python — this is the raw ingest API's shape, not a Python-idiomatic wrapper, since no Python SDK method exists yet.
</Note>

## Next steps

* [Value](/dashboard/value) — the dashboard page this data powers, with per-agent and per-model breakdowns
* [API Reference](/api-reference/introduction) — the **Outcomes** group covers the full request/response contract for `POST /v1/ingest/outcomes` and the two read endpoints
* [Agent tracing](/sdk/agent-tracing) — `withAgent`, for producing the traces you'll later attribute outcomes to
