Integrate the TypeScript SDK
Use @caracalai/sdk in Node applications that need agent sessions, delegation, Gateway routing, or Caracal context propagation.
Install
Section titled “Install”npm install @caracalai/sdkConfigure
Section titled “Configure”The SDK resolves configuration in this order:
new Caracal({ clientSecret: ... })explicit options.new Caracal({ configPath }).CARACAL_CONFIGor acaracal.tomlin the default config path.- Environment variables.
Connect and spawn
Section titled “Connect and spawn”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.
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?.
Long-lived service agents
Section titled “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.
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:
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
Section titled “Spawn a narrowed child”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
Section titled “Route through the Gateway”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
Section titled “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 and Implement Multi-Agent Delegation.

