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

# LangChain and LangGraph

> Govern a multi-agent LangChain or LangGraph graph with one callback handler: every model call and paid tool is checked before it runs, and its usage is reported as the agent that made it.

`StiggLangChainCallbackHandler` governs a LangChain or LangGraph run. Add it to the run's `callbacks`, and LangChain passes it to every agent in the run, including agents started by other agents. Each model call and paid tool is then checked before it runs, and its usage is reported as the agent that made it. For how Stigg governs a team of agents and what it's used for, see [Agent frameworks](https://docs.stigg.io/documentation/ai-agents/overview.md#agent-frameworks).

## Key terms

| Term | Meaning |
| - | - |
| Run | One call to `invoke`, such as answering one user message. A run includes everything the agents do for it. |
| Step | One model call or one tool call inside a run. Stigg checks each step and reports its usage. |
| Graph | The agents and the connections between them. LangGraph builds agents as graphs. |
| Node | One box in a hand-built LangGraph graph. A node usually runs one agent or a set of tools. |
| Callback handler | An object LangChain notifies when each step starts and ends. Stigg's handler uses those notifications to check access and report usage. |

## Quick start

You need a LangChain or LangGraph app, a Stigg server API key, and a credit type in Stigg, here `credits`, that your customers have.

**1. Install the SDK**

```bash theme={null}
npm install @stigg/agent-sdk-js
```

`@langchain/core` 1.x is an optional peer dependency. The handler uses the copy your app already has.

**2. Create the Stigg client**

Create it once, when your server starts:

```typescript theme={null}
import { StiggAgentSDK } from '@stigg/agent-sdk-js';

const stigg = new StiggAgentSDK(process.env.STIGG_SERVER_API_KEY);
```

**3. Create the handler**

Create the handler once too, and tell it what to check each step against:

```typescript theme={null}
import { StiggLangChainCallbackHandler } from '@stigg/agent-sdk-js/langchain';

const stiggHandler = new StiggLangChainCallbackHandler({
  stigg,
  stiggContext: { entitlement: { currencyId: 'credits' } },
});
```

**4. Pass the handler and the customer on every run**

Your agent needs no changes. Add the handler to `callbacks`, and the customer the run is for to `metadata`:

```typescript theme={null}
import { createAgent } from 'langchain';

// Your existing LangChain agent.
const agent = createAgent({ name: 'lead', model, tools });

await agent.invoke(input, {
  callbacks: [stiggHandler],   // next to any other handler, such as Langfuse or LangSmith
  metadata: { stiggContext: { customerId: 'customer-acme' } },
});
```

The customer changes per request, so it goes on the run, not on the handler. Take it from the logged-in user on your server. See [Naming the customer safely](#naming-the-customer-safely). A process that serves one customer can set a default in the handler's `stiggContext: { customerId }` instead. A run with neither is denied before its first step.

**5. Check that it works**

Run the agent once. Then confirm in Stigg that the customer's `credits` balance went down. If it didn't, the token counts the handler reports aren't converted into credits yet. See [The credit balance does not go down](https://docs.stigg.io/documentation/ai-agents/usage-metering.md#the-credit-balance-does-not-go-down).

To see each decision while you test, add a hook to the handler:

```typescript theme={null}
hooks: { onDecision: (decision, context) => console.log(context.agent, decision.hasAccess) },
```

Each step of a run is then handled as follows:

| Step | Before it runs | After it succeeds |
| - | - | - |
| Model call | Checked against the entitlement, if one is set | Token usage reported, through the `usageMapper` if one is set |
| Paid tool | Checked against the tool's entitlement, if it has one | The tool's `usage` reported, or one unit |
| Any other tool | Nothing | Nothing |

A step that fails reports no usage.

## Naming the customer safely

The customer ID decides who pays for a run. If it comes from the user's request, a user can send another customer's ID and charge their usage to that customer. Always set it on your server, from the logged-in user.

* **In your own server**, set `metadata.stiggContext.customerId` where you call `invoke`, from your session or auth layer.
* **On LangGraph Platform**, the client sends the run's `metadata`, so the client could name any customer. Set `stiggContext` in your [custom auth](https://langchain-ai.github.io/langgraph/concepts/auth/) handler from the authenticated identity, and overwrite any value the client sent. The client can also name the run, so also set [`rootAgent`](#a-budget-per-agent) on the handler.
* **With `defaultPlanId`**, every new customer ID a run names becomes a customer on that plan. Make the default plan a free one, so a made-up ID never starts a paid subscription.

The SDK also limits what a run can set. On `invoke`, the run's `stiggContext` can set only `customerId` and `correlationId`. Any other field throws, because a client can write that metadata. Set entitlements, mappers and agent IDs on the handler or under `agents`.

## Configurations

Each configuration below adds to the one before it.

### Metering only

```typescript theme={null}
new StiggLangChainCallbackHandler({ stigg, stiggContext: {} });
```

With no `entitlement`, nothing is checked or denied. Every model call is reported, and the agents run as they would without Stigg. By default each call reports LangChain's own counters: `input_tokens`, `output_tokens` and `total_tokens`. Create meters for those names in Stigg, or convert them with a `usageMapper`.

### One budget for the whole team

```typescript theme={null}
import { unitsPerTokens } from '@stigg/agent-sdk-js';

new StiggLangChainCallbackHandler({
  stigg,
  stiggContext: {
    entitlement: { currencyId: 'credits' },
    usageMapper: unitsPerTokens('credits', 1_000),   // one credit per 1,000 tokens
    ingestMode: 'sync',
  },
});
```

Every model call, by the lead and by every other agent, is checked against the customer's credits, and its usage is reported against them.

* **`usageMapper`** converts a model call's token counts into what is reported, here credits. See [Usage mappers](https://docs.stigg.io/documentation/ai-agents/usage-metering.md#usage-mappers).
* **`ingestMode: 'sync'`** reports each call before the next check, so fast agents can't run far past the limit. See [Ingest modes](https://docs.stigg.io/documentation/ai-agents/usage-metering.md#ingest-modes).

### A budget per agent

To give each agent its own budget, register the agents in Stigg, then tell the handler which LangChain agent is which Stigg agent.

**Step 1. Register the agents.** Each agent is a Stigg [entity](https://docs.stigg.io/documentation/governance/core-concepts.md#entities) of type `agent`. Create a parent agent whose budget caps the whole team, then one agent per team member with its own budget under that parent. Run this once per customer, for example when the customer signs up:

```typescript theme={null}
const credits = { currencyId: 'credits' };

// The parent: the whole team can spend at most 160 credits.
await stigg.register({
  customerId,
  agentId: 'agent-orchestrator',
  entityTypeId: 'agent',
  budget: { entitlement: credits, limit: 160 },
});

// Each team member, with its own limit, under the parent.
for (const [agent, limit] of Object.entries({ lead: 100, researcher: 10, writer: 20 })) {
  await stigg.register({
    customerId,
    agentId: `agent-${agent}`,
    entityTypeId: 'agent',
    budget: { entitlement: credits, limit, parentId: 'agent-orchestrator' },
  });
}
```

**Step 2. Map LangChain's agents to Stigg's.** Under `agents`, each key is an agent's name in LangChain, and its `agentId` is the Stigg agent registered in step 1:

```typescript theme={null}
const stiggHandler = new StiggLangChainCallbackHandler({
  stigg,
  stiggContext: { attributionKey: 'agentId', entitlement: credits, usageMapper, ingestMode: 'sync' },
  rootAgent: 'lead',
  agents: {
    lead: { stiggContext: { agentId: 'agent-lead' } },
    researcher: { stiggContext: { agentId: 'agent-researcher' } },
    writer: { stiggContext: { agentId: 'agent-writer' } },
  },
});
```

* **`attributionKey`** is the key your `agent` entity type uses to identify an agent, here `agentId`. It's set once on the entity type in Stigg. See [Entity types](https://docs.stigg.io/documentation/governance/core-concepts.md#entity-types).
* **`rootAgent`** names the agent that the top-level run always counts as. See the rules below.

Each step is now checked against its agent's budget and the parent's. When the researcher runs out, its steps are denied, and the lead and writer keep working. For more on budgets and parents, see [Usage governance](https://docs.stigg.io/documentation/ai-agents/usage-governance.md).

The handler decides which agent a step belongs to by these rules:

* **Agents are matched by name.** A key in `agents` must be the `name` given to `createAgent` or `createReactAgent`, or the node's name in a hand-built LangGraph graph.
* **A step belongs to the nearest named agent above it.** If the lead starts the researcher, the researcher's steps count against the researcher, even when the lead passes its own config along. The `lc_agent_name` in a run's metadata never decides which agent a step counts against, because a caller can set it.
* **Name every agent.** An agent with no name, or with a name `agents` doesn't list, counts as the nearest listed agent above it, usually the one that started it.
* **Set `rootAgent`.** Whoever calls `invoke` can name the top-level run, so without `rootAgent` the caller could pick whose budget pays. With `rootAgent: 'lead'`, the top-level run always counts as the lead.
* **An agent's settings are merged over the shared ones**, field by field. An agent that sets only `agentId` still uses the shared `entitlement` and `usageMapper`. Each step is checked once and reported once.
* **An agent missing from `agents`** uses the shared settings alone, so its spend counts against the customer, not an agent, unless the shared settings name an agent. The SDK logs a warning once.
* **`idempotencyKey` is not allowed** in any of these settings, because every step would share it and only the first would be reported. The SDK keys each step by its own ID.

### A hand-built LangGraph graph

In a hand-built `StateGraph`, each node is an agent. Use the same handler, with `agents` keyed by node name:

```typescript theme={null}
const graph = new StateGraph(Turn)
  .addNode('lead', lead)
  .addNode('researcher', researcher)
  .addNode('tools', new ToolNode([search]))
  .addNode('writer', writer)
  // ...edges
  .compile();

await graph.invoke(input, { callbacks: [stiggHandler], metadata: { stiggContext: { customerId } } });
```

Name each node after the agent it runs, including a node that runs a subgraph. A tool that runs in its own node, such as LangGraph's `ToolNode`, counts against the agent whose model asked for it, not to the `tools` node.

## Tools

Model calls are always checked and reported. A tool is only checked and reported when `tools` lists it, because most tools, such as a calculator or a lookup in your own database, cost nothing.

```typescript theme={null}
new StiggLangChainCallbackHandler({
  stigg,
  stiggContext: { entitlement: credits, usageMapper, ingestMode: 'sync' },
  tools: {
    // Checked, then one unit per call.
    send_email: { currencyId: 'credits' },

    // Checked, then reported as one credit per search result.
    tavily_search: {
      entitlement: { currencyId: 'credits' },
      usage: ({ output }) => ({ credits: output.results.length }),
    },

    // Reported only: never checked.
    sql_query: { usage: ({ output }) => ({ 'rows-read': output.rowCount }) },

    // Every tool not listed here...
    '*': { currencyId: 'credits' },
    // ...except the tool that runs the researcher agent, whose model calls are already reported.
    researcher: false,
  },
});
```

| Entry | Checked before | Reported after |
| - | - | - |
| An entitlement | Yes | One unit against it |
| `{ entitlement, usage }` | Yes | What `usage` returns |
| `{ usage }` | No | What `usage` returns |
| `false` | No | Nothing |
| `'*'` | As its value says | As its value says |

* **`usage` prices a tool call.** It receives `{ name, input, output }`, with the tool's arguments and result, and returns amounts by feature or credit ID. A model call's `usageMapper` does the same job from token counts. If `usage` throws, the error goes to `onError` and the run continues.
* **Avoid counting usage twice.** A common pattern wraps an agent in a tool, such as `tool(() => researcher.invoke(...))`, so the lead can call it. The researcher's model calls are already reported as the researcher, so reporting the tool too would count the same work twice. If `'*'` would cover that tool, set it to `false`.
* **Per agent.** `agents.<name>.tools` is merged over the shared `tools`, name by name. An agent can add its own paid tool, or set a shared one to `false`. `'*': false` only opts the agent out of the catch-all.
* **Attribution.** A tool counts against the agent whose model asked for it.
* **Packaged tools.** Add `'*'` so packaged tools such as `TavilySearch` are always covered, including after the graph resumes. The handler names these tools from the model's request, so their own entries apply too.
* **Each tool call is reported once**, keyed by the model's tool-call ID. A retried or resumed call is checked again but reported under the same key.
* **Entries are validated when the handler is built.** An entry with neither an entitlement nor a `usage` is rejected, and so is `{ currencyId, usage }`. Write that as `{ entitlement: { currencyId }, usage }`.

## When access is denied

What your code can do depends on which step was denied:

| Denied step | What happens | What your code can do |
| - | - | - |
| A paid tool | The tool call fails | Tell the model the tool isn't available, so it carries on without it |
| Another agent's model call | That agent's `invoke` throws | Fall back to another agent, or return a reduced answer |
| The top agent's model call | Your `invoke` throws | Tell the user how to get more credits |

Stigg reports a denial by throwing an `AccessDeniedError` from inside the step, and your code decides what to do with it. LangChain often wraps that error in its own, so use `stigg.denial(err)` to find it. It returns the denial, with a `message` you can show, or `undefined` for any other error.

In this example the researcher runs as a tool of the lead. When the researcher is out of budget, a cheaper `generalist` agent, with no paid search, answers instead:

```typescript theme={null}
const research = tool(
  async ({ brief }) => {
    const input = { messages: [{ role: 'user', content: brief }] };
    try {
      const result = await researcher.invoke(input);
      return result.messages.at(-1).content;
    } catch (err) {
      const decision = stigg.denial(err);
      if (!decision) throw err;   // not a Stigg denial
      const result = await generalist.invoke(input);
      return `(Unsourced: ${decision.message})\n${result.messages.at(-1).content}`;
    }
  },
  { name: 'researcher', description: 'Research a question.', schema: z.object({ brief: z.string() }) },
);
```

At the top of the graph, catch the denial and show it to the user:

```typescript theme={null}
try {
  await lead.invoke(input, { callbacks: [stiggHandler], metadata: { stiggContext: { customerId } } });
} catch (err) {
  const decision = stigg.denial(err);
  if (!decision) throw err;
  showUpgradePrompt(decision.message);   // your own UI
}
```

In a hand-built LangGraph graph, catch the denial inside the node, save it in the graph's state, and use a conditional edge to route around it, such as `state.refused ? 'generalist' : 'writer'`.

If Stigg can't be reached to check, the SDK follows `checkErrorAction`, which lets the step run by default. See [Fallback strategy](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md#fallback-strategy). Reporting usage never throws into the run.

## Observability

* **Hooks.** `hooks: { onDecision, onIngest }` are called for every check and every report. `context.agent` and `context.tool` say which agent and tool the step belonged to.
* **Tracing.** Stigg emits OpenTelemetry spans, so each check and each report appears in any tracing tool that reads OpenTelemetry, next to the agents' own steps. A check shows up as a `stigg.check` span and a report as a `stigg.ingest` span. A span is one timed entry in a trace. A denied step's span is marked as an error. This needs `@opentelemetry/api`, an optional peer dependency.
* **Flushing.** Call `await stigg.flush()` before the process exits, so no report is lost.

Some tools export only the spans of libraries they recognize. In those, add Stigg's tracer name, `STIGG_TRACER_NAME`, to the filter. For example, in Langfuse:

```typescript theme={null}
import { LangfuseSpanProcessor, isDefaultExportSpan } from '@langfuse/otel';
import { STIGG_TRACER_NAME } from '@stigg/agent-sdk-js';

const processor = new LangfuseSpanProcessor({
  shouldExportSpan: ({ otelSpan }) =>
    isDefaultExportSpan(otelSpan) || otelSpan.instrumentationScope.name === STIGG_TRACER_NAME,
});
```

## Best practices

* **Use chat models for exact metering.** Chat models report usage for every call. For LangChain's text-completion models, avoid streaming, and expect a batch of prompts to report its whole usage on the first prompt. A call with no usage goes to `onError` instead of being reported as zero.
* **Set the customer on your server.** The customer ID decides who pays for a run. Take it from the logged-in user, never from the request, or a user can charge their usage to another customer. See [Naming the customer safely](#naming-the-customer-safely).
* **Keep parallel spend within budget.** Each check reads the balance at that moment, so steps that start together can each pass. Keep limits a little below the true cap, or [reserve credits](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md#reserving-credits-for-a-task) for the whole task. See [Caching and accuracy](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md#caching-and-accuracy).
* **Choose small credit units.** Agent budgets count whole units and round each call up, so define your credit type in small units, such as thousandths of a dollar, to keep rounding negligible. See [Usage governance](https://docs.stigg.io/documentation/ai-agents/usage-governance.md).
* **Roll out without denials.** Start with metering only, or register agents with no limit, then add limits once usage looks right. See [Observe-only rollout](https://docs.stigg.io/documentation/ai-agents/usage-governance.md#observe-only-rollout).


## Related topics

- [JavaScript](https://docs.stigg.io/api-and-sdks/changelog/agent-sdk/javascript.md)
- [AI agents](https://docs.stigg.io/documentation/ai-agents/overview.md)
- [Providers and frameworks](https://docs.stigg.io/documentation/ai-agents/providers/overview.md)
- [Usage governance](https://docs.stigg.io/documentation/ai-agents/usage-governance.md)
- [Google ADK](https://docs.stigg.io/documentation/ai-agents/orchestrators/google-adk.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.