Local first-action quickstart

One action. Retry it. Verify one effect.

Use the actual Once SDK without a Once API key. This controlled example writes a fake ticket provider’s receipt to a separate local journal.

This is an SDK verification, not an external-provider proof. Node.js 24.15+ is required. Keep the same working directory and durable SQLite file for every run. No network provider, payment or hosted subscription is used.

1. Check Node and install

Use a fresh disposable directory. These commands work in a terminal or PowerShell.

node --version
mkdir once-first-action
cd once-first-action
npm init -y
npm install @once-agent/[email protected]

If Node is older than 24.15, update it before running local protection. The general hosted SDK has a different requirement; this guide uses local SQLite.

2. Save the complete example

Download first-action.mjs into that directory, or copy the full code below into a file named first-action.mjs.

Complete copyable example
import assert from "node:assert/strict";
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { protectLocal } from "@once-agent/sdk";

// Controlled fake provider: its effect journal is separate from Once state.
// This verifies the installed SDK, not a real payment or ticket integration.
const directory = path.resolve(".once-first-action");
mkdirSync(directory, { recursive: true });
const statePath = path.join(directory, "operations.sqlite");
const mode = process.argv[2] || "verify";
assert.ok(["verify", "restart", "unknown", "reconcile"].includes(mode));
const journal = name => path.join(directory, `${name}.jsonl`);
const rows = name => existsSync(journal(name))
  ? readFileSync(journal(name), "utf8").trim().split("\n").filter(Boolean).map(JSON.parse)
  : [];

// In your integration use a durable intentional action ID, scoped by tenant
// and action type, and bind EVERY input that can change the external effect.
const identity = input => `demo-tenant:create-ticket:${input.intentId}`;
const payload = input => ({ intentId: input.intentId, title: input.title });
const createTicket = protectLocal(async input => {
  const result = { number: "TICKET-1", title: input.title };
  appendFileSync(journal("confirmed"), JSON.stringify(result) + "\n");
  return result;
}, { id: identity, payload, statePath });

const original = { intentId: "ticket-001", title: "Investigate timeout" };
if (mode === "verify") {
  const result = await createTicket(original);
  assert.deepEqual(await createTicket(original), result);
  await assert.rejects(createTicket({ ...original, title: "Changed title" }), { code: "CONFLICT" });
  assert.equal(rows("confirmed").length, 1);
  console.log("CONFIRMED: TICKET-1\nREPLAY: original result\nCONFLICT: no write dispatched\nProvider effects: 1");
} else if (mode === "restart") {
  assert.equal(rows("confirmed").length, 1, "Run verify first; keep the state directory.");
  assert.deepEqual(await createTicket(original), rows("confirmed")[0]);
  assert.equal(rows("confirmed").length, 1);
  console.log("RESTART REPLAY: TICKET-1\nProvider effects: 1");
} else {
  // The fake provider commits, THEN its acknowledgement is lost.
  const lostInput = { intentId: "ticket-lost-ack", title: "Lost acknowledgement" };
  const lost = protectLocal(async input => {
    const result = { number: "TICKET-LOST", title: input.title };
    appendFileSync(journal("ambiguous"), JSON.stringify(result) + "\n");
    throw new Error("Simulated acknowledgement loss after commit");
  }, {
    id: identity, payload, statePath,
    ...(mode === "reconcile" ? {
      // Authoritative ONLY for this controlled fake provider. A real provider
      // needs its own trustworthy, read-only lookup and concurrency contract.
      reconcile: () => rows("ambiguous").length === 1
        ? { state: "CONFIRMED", result: rows("ambiguous")[0] }
        : { state: "UNKNOWN" },
    } : {}),
  });
  if (mode === "unknown") {
    await assert.rejects(lost(lostInput));
    await assert.rejects(lost(lostInput), { code: "UNKNOWN" });
    assert.equal(rows("ambiguous").length, 1);
    console.log("UNKNOWN: retry blocked\nAmbiguous provider effects: 1");
  } else {
    assert.equal(rows("ambiguous").length, 1, "Run unknown first.");
    assert.equal((await lost(lostInput)).number, "TICKET-LOST");
    assert.equal(rows("ambiguous").length, 1);
    console.log("RECONCILED: TICKET-LOST\nAmbiguous provider effects: 1");
  }
}

The id binds a stable intentional action, scoped by tenant and action type. The payload binds both intent and title. The wrapped caller is the only route to the fake provider. A successful receipt is plain, replayable data.

3. Verify confirmed replay and conflict

node first-action.mjs

Expected output:

CONFIRMED: TICKET-1
REPLAY: original result
CONFLICT: no write dispatched
Provider effects: 1

Inspect .once-first-action/confirmed.jsonl: there is one provider receipt. This journal is separate from operations.sqlite, the Once state file.

4. Restart the process

node first-action.mjs restart
RESTART REPLAY: TICKET-1
Provider effects: 1

This second command runs in a fresh process. Do not delete state or change its path. A persisted confirmed receipt is returned without a second provider dispatch.

5. Commit, lose the acknowledgement, then retry

node first-action.mjs unknown
UNKNOWN: retry blocked
Ambiguous provider effects: 1

This separate fake-provider action commits before throwing. The next attempt must remain blocked. Keep the same identity; do not call the provider directly or create a fresh identity to evade the block.

6. Confirm provider truth

node first-action.mjs reconcile
RECONCILED: TICKET-LOST
Ambiguous provider effects: 1

A read-only lookup of this controlled provider’s journal authoritatively confirms its receipt. This assumption belongs to the fake provider, not arbitrary APIs. Without confirmation, the action stays UNKNOWN. Local mode does not automatically redispatch an ambiguous write on ABSENT.

7. Protect a real action

  1. Replace the fake provider with one existing consequential provider operation that returns a confirmed, JSON-safe receipt. Keep its native idempotency safeguards.
  2. Choose a durable ID for one intentional action, scoped by tenant and operation. A separate legitimate intent must have a different ID; retries must keep the same ID.
  3. Bind every input that changes the effect, including recipient, amount, resource and account where applicable. Do not infer identity from a per-attempt tool-call ID.
  4. Use an explicit persistent statePath, shared by cooperating callers on one machine. Network filesystems and separate host state are unsupported.
  5. Route all callers through the wrapper. Remove the original unprotected tool from the agent’s exposed capabilities. Internal provider retries and multiple effects inside one wrapper need their own safety analysis.
  6. Supply authoritative read-only reconciliation when available. A timeout, stale lookup or unsupported 404 does not prove absence. Unknown outcomes must not silently bypass protection.
  7. In the provider’s disposable sandbox, verify first action, same-intent retry, changed-input conflict and fresh-process replay using provider evidence. Test acknowledgement loss only when safe.
Success means your actual caller reaches the protected boundary and provider evidence confirms the expected effects. Installing the SDK, finding a candidate or generating a companion file does not prove active protection.

Full local-function contract · GitHub recovery example

Protect an agent’s existing tools

The local Connect path requires Node.js 24.15+, durable same-machine SQLite and trusted identity/effect metadata. It classifies supported tools as BYPASS, PROTECT or UNKNOWN. Unresolved toolsets fail closed rather than returning a partially protected registry. No Once API key is needed for this local route.

Use only the returned connected tools in the agent. Automatic classification does not invent business identity or prove authoritative provider truth.

Open the complete Connect guide

Set up Codex, Claude Code or Cursor

The plugin/MCP layer assesses and plans supported integration. Local assessment needs no key; hosted setup needs a Once API key. The server’s presence does not automatically protect other tools or replace the execution boundary.

Codex

codex plugin marketplace add stringsofthemind-oss/once --ref main
codex plugin add once@once-agent

Claude Code

claude plugin marketplace add stringsofthemind-oss/once
claude plugin install once@once-agent

Claude setup details · Cursor project MCP configuration · Codex plugin instructions

After setup, ask for a read-only project assessment, review identity/effect semantics, wire supported operations, then verify a real protected action. The selected local or hosted runtime determines where state lives.

Evaluate a supported hosted operation

Hosted execution requires a Once API key and explicit supported provider setup. The SDK sends operation identity, provider alias and action payload to the configured Once API. Current hosted HTTP provider registration includes its base URL and provider token.

The hosted service manages execution state; the homepage does not promise a storage jurisdiction, retention period, encryption design, audit or SLA. Evaluate those requirements before sending sensitive production data. If the service cannot be reached after bounded retries, the SDK raises an error; it does not silently dispatch directly.

Recovery and any permission to execute after an absence observation depend on the specific downstream contract. Do not apply a generic ABSENT → EXECUTE rule to local protection.

Check provider supportActivate hosted sandbox

Hosted activation is a separate Stripe test checkout flow. It is optional for learning and local verification. Use test details only; do not enter real card data.

Assess your codebase first

npx --yes --package=@once-agent/sdk once doctor .

Default Doctor assessment is local, requires no Once API key, does not upload source and does not modify application source. It reports likely consequential candidates and supported next steps. Discovery is not protection. Follow the candidate-specific guide; unsupported shapes may require reviewed local integration or an adapter.

Keep protection active

Preserve the state path, logical IDs and complete effect payload across deploys. Monitor unresolved UNKNOWN outcomes and conflicts. Verify provider truth before recovery; never treat any error as permission to bypass the wrapper.

Inspect evidence and limitations · Report an integration problem