---
title: "Integrate the TypeScript SDK"
url: "https://docs.caracal.run/guides/sdk-typescript/"
markdown_url: "https://docs.caracal.run/markdown/guides/sdk-typescript.md"
description: "Install @caracalai/sdk, load a runtime profile, spawn agents, delegate authority, and inject Caracal headers."
page_type: "page"
concepts: []
requires: []
---

# Integrate the TypeScript SDK

Canonical URL: https://docs.caracal.run/guides/sdk-typescript/
Markdown URL: https://docs.caracal.run/markdown/guides/sdk-typescript.md
Description: Install @caracalai/sdk, load a runtime profile, spawn agents, delegate authority, and inject Caracal headers.
Page type: page
Concepts: none
Requires: none

---

Use `@caracalai/sdk` in Node applications that need agent sessions, delegation, Gateway routing, or Caracal context propagation.

## Install

```bash
npm install @caracalai/sdk
```

## Configure

The SDK resolves configuration in this order:

1. `new Caracal({ clientSecret: ... })` explicit options.
2. `new Caracal({ configPath })`.
3. `CARACAL_CONFIG` or a `caracal.toml` in the default config path.
4. Environment variables.

## Connect and spawn

```ts
import { Caracal } from "@caracalai/sdk";

const caracal = new Caracal();

await caracal.spawn(async () => {
  const headers = await caracal.headersAsync();

  await fetch("https://api.example.com/tickets", {
    headers,
  });
});
```

`spawn()` creates an agent session, binds the Caracal context while the callback runs, and terminates the session when the callback exits.

To make agents distinguishable in policy and audit, pass `labels`. These become `input.principal.labels`, so several agents under one application stay separable without one application per agent. `labels` are descriptive, for policy and audit, not grants; authority always comes from scopes and delegation. The session lifecycle is handled for you: `spawn()` records a `task` session, while `spawnService()` records a heartbeat-leased `service`.

```ts
await caracal.spawn(async () => {
  const headers = await caracal.headersAsync();
  await fetch("https://api.example.com/tickets", { headers });
}, { labels: ["refund-agent"] });
```

See [If many agents share one managed application, can policy and audit still tell them apart?](/reference/faq/#faq-008).

## Long-lived service agents

Daemons and workers that outlive a single request use `spawnService()` instead of `spawn()`. It returns a handle you own: keep the session alive by calling `heartbeat()` on a timer and retire it with `close()`. Each `heartbeat()` **renews the lease** — it extends the deadline the coordinator measures liveness against; it does not merely check status. A service session is reaped by the coordinator only if it stops heartbeating before its lease expires. Unlike a `task`, a `service` is **not** subject to the wall-clock TTL sweeper; its lifetime is governed entirely by the lease, so a faithfully heartbeated service runs until you close it.

```ts
const svc = await caracal.spawnService({ labels: ["billing-worker"] });
try {
  while (running) {
    await svc.heartbeat();
    await doWork(await caracal.headersAsync());
    await sleep(30_000);
  }
} finally {
  await svc.close();
}
```

When your code spends long stretches awaiting a provider (a streaming response, a slow tool), renewing the lease from the same loop can starve. Pass `heartbeatIntervalMs` to renew the lease from an independent timer, so the lease stays current even while your code is blocked on a long `await`:

```ts
const svc = await caracal.spawnService({ labels: ["voice-worker"], heartbeatIntervalMs: 30_000 });
try {
  for await (const chunk of stream(await caracal.headersAsync())) {  // lease renews in the background
    await handle(chunk);
  }
} finally {
  await svc.close();
}
```

Auto-heartbeat is opt-in (omit `heartbeatIntervalMs` to renew manually). Transient renewal errors are logged and retried on the next tick rather than crashing the worker. A renewal cannot run while the event loop is blocked synchronously; in that case the lease correctly lapses, which is the liveness signal working as intended.

## Spawn a narrowed child

```ts
import { Grant } from "@caracalai/sdk";

await caracal.spawn(async () => {
  await caracal.spawn(async () => {
    const headers = await caracal.headersAsync();
    await fetch("https://api.example.com/tickets", { headers });
  }, {
    grant: Grant.narrow(["tickets:read"], {
      resourceId: "https://api.example.com/tickets",
      constraints: { maxHops: 1, budget: 5 },
      ttlSeconds: 600,
    }),
  });
});
```

A plain `spawn()` runs the child under its parent's effective authority — the application's authority for a root parent, or the parent's narrowed slice when the parent was itself narrowed (transitive least-privilege). Pass `grant: Grant.narrow(...)` only when the child should hold a smaller subset, or `Grant.none()` for a child with no inherited authority. Use `delegate()` when you need to grant authority to an agent session that already exists, typically in another application.

## Route through the Gateway

```ts
const request = caracal.gatewayRequest("https://api.example.com/tickets", "/tickets");

await fetch(request.url, {
  headers: {
    ...request.headers,
    ...(await caracal.headersAsync({ allowRoot: true })),
  },
});
```

For provider SDKs that accept a custom `fetch`, pass `caracal.transport()` so Caracal headers and Gateway routing are applied automatically.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| `Caracal.fromEnv: missing ...` | Confirm the runtime profile or required `CARACAL_*` variables are present. |
| Root headers rejected | Call `headersAsync({ allowRoot: true })` only when service-root identity is intentional. |
| Delegation fails | Ensure the call runs inside `spawn()` or another bound context. |
| Gateway request misses resource | Confirm `gateway_url`, resource bindings, and `X-Caracal-Resource`. |

Related pages: [Add SDK to Your App](/get-started/add-sdk-to-your-app/) and [Implement Multi-Agent Delegation](/guides/delegation/).
