Skip to content

Integrate the TypeScript SDK

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

Terminal window
npm install @caracalai/sdk

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.
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?.

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.

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.

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.

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

Related pages: Add SDK to Your App and Implement Multi-Agent Delegation.