Skip to content

TypeScript SDK

The official TypeScript SDK is published as @onceonly/onceonly-sdk on npm.

Terminal window
npm i @onceonly/onceonly-sdk

The package requires Node.js 20+.

import { OnceOnly } from "@onceonly/onceonly-sdk";
const client = new OnceOnly({
apiKey: process.env.ONCEONLY_API_KEY!
});

If you self-host, pass a custom baseUrl. The SDK accepts either the root URL or a URL that already includes /v1:

const client = new OnceOnly({
apiKey: process.env.ONCEONLY_API_KEY!,
baseUrl: "http://localhost:8080"
});

You can also configure timeout, fail-open behavior for checkLock(), and retry/backoff:

const client = new OnceOnly({
apiKey: process.env.ONCEONLY_API_KEY!,
timeoutMs: 5000,
failOpen: true,
maxRetries429: 3,
retryBackoffSec: 0.5,
retryMaxBackoffSec: 10
});

const lock = await client.checkLock({
key: "payment:invoice:INV-123",
ttl: 3600,
meta: { invoice_id: "INV-123", amount_usd: 99.99 }
});
if (lock.duplicate) {
throw new Error(`Duplicate (firstSeenAt=${lock.firstSeenAt})`);
}
await chargeCustomer();

Async alias:

const lock = await client.checkLockAsync({
key: "payment:invoice:INV-123",
ttl: 3600,
meta: { invoice_id: "INV-123" }
});

const run = await client.ai.run({
key: "support_chat:abc123",
ttl: 1800,
metadata: {
run_id: "run_support_001",
agent_id: "support_bot"
}
});
console.log(run.status, run.leaseId, run.version);
const final = await client.ai.runAndWait({
key: "support_chat:abc123",
ttl: 1800,
metadata: {
run_id: "run_support_001",
agent_id: "support_bot"
},
timeout: 120
});
console.log(final.status, final.errorCode, final.result);
const res = await client.ai.runTool({
agentId: "support_bot",
tool: "send_email",
args: {
to: "user@example.com",
subject: "Hello"
},
runId: "run_support_001",
spendUsd: 0.001
});
if (res.allowed) {
console.log(res.decision, res.result);
} else {
console.log(res.decision, res.policyReason);
}

Local exactly-once execution with your own function

Section titled “Local exactly-once execution with your own function”
const result = await client.ai.runFn(
"email:welcome:user_123",
async () => ({ sent: true }),
{
ttl: 300,
metadata: { tool: "send_welcome_email" },
waitOnConflict: true,
timeout: 60
}
);
console.log(result.status, result.result);
const lease = await client.ai.lease("support_chat:abc123", 1800, { chat_id: "abc123" });
const status = await client.ai.status("support_chat:abc123");
const result = await client.ai.result("support_chat:abc123");
await client.ai.extend("support_chat:abc123", String(lease.lease_id), 1800);
await client.ai.complete("support_chat:abc123", String(lease.lease_id), { status: "done" });
// or
await client.ai.fail("support_chat:abc123", String(lease.lease_id), "chat_failed");
// optional cancel path
await client.ai.cancel("support_chat:abc123", String(lease.lease_id), "manual_stop");

Governance (policies, tools, agent controls)

Section titled “Governance (policies, tools, agent controls)”
const policy = await client.gov.upsertPolicy({
agent_id: "support_bot",
allowed_tools: ["send_email", "create_ticket"],
blocked_tools: ["delete_user"],
max_actions_per_hour: 100,
max_spend_usd_per_day: 50
});
console.log(policy.agentId, policy.maxActionsPerHour);
const templated = await client.gov.policyFromTemplate(
"new_support_bot",
"moderate",
{ blocked_tools: ["delete_user"] }
);
console.log(templated.agentId, templated.policy);
const allPolicies = await client.gov.listPolicies();
const onePolicy = await client.gov.getPolicy("support_bot");
await client.gov.disableAgent("support_bot", "incident_123");
await client.gov.enableAgent("support_bot", "incident_resolved");
const logs = await client.gov.agentLogs("support_bot", 100);
const metrics = await client.gov.agentMetrics("support_bot", "day");
console.log(logs.length, metrics.totalActions, metrics.blockedActions);
const tool = await client.gov.createTool({
name: "send_email",
scope_id: "global",
url: "https://your-domain.com/tools/send-email",
auth: { type: "hmac_sha256", secret: process.env.TOOL_SECRET! },
timeout_ms: 15000,
max_retries: 2,
enabled: true,
description: "Send transactional email"
});
console.log(tool.name, tool.enabled);

List, fetch, toggle, and delete:

const tools = await client.gov.listTools("global");
const oneTool = await client.gov.getTool("send_email", "global");
const disabled = await client.gov.toggleTool("send_email", false, "global");
const deleted = await client.gov.deleteTool("send_email", "global");

See also: Policy Templates | Implementing Tool Backends


const profile = await client.me();
const prefs = await client.updateNotifications({
emailNotificationsEnabled: true,
toolErrorNotificationsEnabled: true,
runFailureNotificationsEnabled: false
});
const makeUsage = await client.usage("make");
const aiUsage = await client.usage("ai");
const allUsage = await client.usageAll();
const summary = await client.metrics("2026-04-01", "2026-04-30");
console.log(profile.email);
console.log(makeUsage.usage, aiUsage.usage, allUsage.plan);
console.log(summary);

Structured run events:

const event = await client.postEvent({
runId: "run_support_001",
type: "tool_call",
status: "start",
step: "step_1",
tool: "send_email",
agentId: "support_bot"
});
const timeline = await client.getRunTimeline("run_support_001", 200, 0);
const recentEvents = await client.events(20, 0);
console.log(event.event_id);
console.log(timeline.total);
console.log(recentEvents.length ?? recentEvents.items?.length ?? 0);

The current SDK wraps:

  • me() / meAsync()
  • updateNotifications() / updateNotificationsAsync()
  • usage() / usageAsync()
  • usageAll() / usageAllAsync()
  • metrics() / metricsAsync()
  • postEvent() / postEventAsync()
  • getRunTimeline() / getRunTimelineAsync()
  • events() / eventsAsync()

The current REST API also has GET /v1/runs, but there is no dedicated listRuns() helper in the SDK yet.


TypeScript SDK methods are async by default. It also exposes explicit async aliases for parity with Python naming:

  • checkLockAsync()
  • aiRunAsync() / aiRunAndWaitAsync()
  • meAsync() / updateNotificationsAsync()
  • usageAsync() / usageAllAsync() / metricsAsync()
  • eventsAsync() / postEventAsync() / getRunTimelineAsync()
  • client.ai.runAsync() / runAndWaitAsync() / runToolAsync() / runFnAsync()
  • client.ai.leaseAsync() / extendAsync() / completeAsync() / failAsync() / cancelAsync()
  • client.gov.*Async() variants for all governance methods

The OnceOnly client exposes convenience aliases for AI runs:

  • aiRun(...) / aiRunAsync(...)
  • aiRunAndWait(...) / aiRunAndWaitAsync(...)

They delegate to client.ai.run(...) and client.ai.runAndWait(...).


The package exports two decorator helpers from @onceonly/onceonly-sdk:

  • idempotent(client, fn, opts) for check-lock
  • idempotentAi(client, fn, opts) for exactly-once local execution with AI leases
import { OnceOnly, idempotent, idempotentAi } from "@onceonly/onceonly-sdk";
const client = new OnceOnly({ apiKey: process.env.ONCEONLY_API_KEY! });
const chargeInvoice = idempotent(
client,
async (invoiceId: string) => ({ status: "paid", invoiceId }),
{
keyPrefix: "payments",
ttl: 3600,
onDuplicate: async (invoiceId) => ({ status: "duplicate", invoiceId })
}
);
const handleSupportChat = idempotentAi(
client,
async (chatId: string, messages: Array<{ role: string; content: string }>) => {
return { status: "resolved", messages: messages.length };
},
{
keyFn: (chatId) => `support_chat:${chatId}`,
ttl: 1800,
metadataFn: (chatId, messages) => ({ chat_id: chatId, message_count: messages.length })
}
);
const out = await handleSupportChat("chat_123", [{ role: "user", content: "Help" }]);
console.log(out.status, out.result);

idempotentAi returns an AiResult, not the raw function output, because completion is normalized through the lease/result flow.


See also: AI Run API | Runs & Events API | Usage API