Availsync

Node SDK

Guard coding-agent runs with one SDK call.

Use @availsync/node before Codex, Claude, Cursor, MCP clients, cron jobs, deploy scripts, or server automations touch a protected repo.

Install

npm install @availsync/node

Environment

AVAILSYNC_API_KEY=avs_live_...
AVAILSYNC_AGENT_ID=agent_uuid
AVAILSYNC_API_URL=https://availsync.dev

Recommended helper

Use withClaim first

Default to repo-level resources. In enforce mode, blocked agents get skip_run. Observe mode currently applies to the Node SDK, MCP work tools, and direct calls to /v1/work/run/start. In observe mode, the callback still runs and receives shadow metadata.

Basic guarded run

Use the SDK helper when your app should run work only after Availsync claims the repo.

import { createAvailsyncClient } from '@availsync/node';

const availsync = createAvailsyncClient({
  apiKey: process.env.AVAILSYNC_API_KEY!,
  agentId: process.env.AVAILSYNC_AGENT_ID!,
  baseUrl: process.env.AVAILSYNC_API_URL,
});

const result = await availsync.work.withClaim(
  'repo:owner/repo',
  {
    reason: 'coding-agent run',
    durationMinutes: 45,
    autoExtendIntervalMs: (45 - 5) * 60_000,
    autoExtendDurationMinutes: 45,
    autoExtendWorkingSet: {
      resource: 'repo:owner/repo',
      mode: 'edit',
      paths: ['frontend/components/Hero.tsx', 'frontend/app/page.tsx'],
      leaseSeconds: 600,
    },
  },
  async ({ claim, shadow }) => {
    if (shadow.wouldHaveBlocked) {
      console.warn('Observe-only: Availsync would have skipped this run.');
    }
    await claim?.updateWorkingSet({
      resource: 'repo:owner/repo',
      mode: 'edit',
      paths: ['frontend/components/Hero.tsx', 'frontend/app/page.tsx'],
      leaseSeconds: 600,
    });
    await doWork();
  },
);

if (result.action === 'skip_run') {
  console.log(result.reason);
}

Scheduled Codex run

Wrap a scheduled Codex task so it exits cleanly when another agent owns the repo.

import { createAvailsyncClient } from '@availsync/node';
import { spawn } from 'node:child_process';
import { once } from 'node:events';

const availsync = createAvailsyncClient({
  apiKey: process.env.AVAILSYNC_API_KEY!,
  agentId: 'AGENT_ID',
  baseUrl: process.env.AVAILSYNC_API_URL,
});

const result = await availsync.work.withClaim(
  'repo:owner/repo',
  {
    reason: 'scheduled Codex automation',
    durationMinutes: 45,
    idempotencyKey: `codex-owner-repo-${new Date().toISOString().slice(0, 13)}`,
  },
  async ({ shadow }) => {
    if (shadow.wouldHaveBlocked) {
      console.warn('Observe-only: Availsync would have skipped this Codex run.');
    }
    const child = spawn('codex', ['exec', 'run the scheduled maintenance task'], { stdio: 'inherit' });
    const [code] = await once(child, 'exit');
    if (code !== 0) throw new Error(`codex exited with ${code}`);
  },
);

if (result.action === 'skip_run') process.exit(0);

GitHub Actions step

Install the SDK in your automation project and use GitHub run id as the idempotency key.

- name: Run guarded agent task
  env:
    AVAILSYNC_API_KEY: ${{ secrets.AVAILSYNC_API_KEY }}
    AVAILSYNC_AGENT_ID: AGENT_ID
    AVAILSYNC_API_URL: https://availsync.dev
  run: |
    npm install @availsync/node
    node ./scripts/guarded-agent-run.mjs

# scripts/guarded-agent-run.mjs
import { createAvailsyncClient } from '@availsync/node';

const availsync = createAvailsyncClient({
  apiKey: process.env.AVAILSYNC_API_KEY,
  agentId: process.env.AVAILSYNC_AGENT_ID,
  baseUrl: process.env.AVAILSYNC_API_URL,
});

const result = await availsync.work.withClaim(
  'repo:owner/repo',
  {
    reason: 'GitHub Actions coding automation',
    idempotencyKey: `github-${process.env.GITHUB_RUN_ID}`,
    durationMinutes: 45,
    autoExtendIntervalMs: (45 - 5) * 60_000,
    autoExtendDurationMinutes: 45,
  },
  async ({ shadow }) => {
    if (shadow.wouldHaveBlocked) {
      console.warn('Observe-only: Availsync would have skipped this run.');
    }
    // Run the coding agent command here.
  },
);

if (result.action === 'skip_run') process.exit(0);

Manual long-running flow

Use start, extend, and finish directly when you need more control than withClaim.

const started = await availsync.work.start({
  resource: 'repo:owner/repo',
  reason: 'long-running agent run',
  durationMinutes: 45,
  idempotencyKey: `agent-owner-repo-${Date.now()}`,
});

if (started.action === 'skip_run') {
  console.log(started.reason);
  process.exit(0);
}

if (started.shadow_mode && started.would_have_blocked) {
  console.warn('Observe-only: Availsync would have skipped this run.');
}

let outcome = 'finished';
try {
  if (started.claim) {
    // Extend before expiry while the long-running task is still active.
    await availsync.work.extend(started.claim.id, { durationMinutes: 45 });
  }
  await doWork();
} catch (error) {
  outcome = 'error';
  throw error;
} finally {
  if (started.claim) {
    await availsync.work.finish(started.claim.id, { outcome });
  }
}

API surface

Methods

createAvailsyncClient({ apiKey, agentId, baseUrl? })

Creates a Node 18+ client. The API key belongs to one agent and should only live in server, CI, MCP, or automation env vars.

work.withClaim(resource, options, fn)

Recommended helper. Starts a guarded run, runs your callback only when action is proceed, and finishes the claim in finally.

work.start({ resource, durationMinutes?, reason?, idempotencyKey?, metadata? })

Low-level start call for custom wrappers. Returns proceed, skip_run, or observe-mode proceed with shadow metadata.

work.check({ resource, durationMinutes?, reason?, metadata? })

Non-mutating preview for custom flows. It tells you whether the same resource would be available, without creating a run claim.

work.get(claimId)

Fetches one claim by id so a resumed automation can verify the current status, expiry, and renewal count before continuing.

work.list({ resource?, status?, limit?, offset? })

Lists claims with pagination. Active claims include their current file working set so dashboards and agents can see what is being edited.

work.extend(claimId, { durationMinutes?, workingSet? })

Extends an active lease when a long-running agent needs more time. Pass workingSet to refresh file claims and detect newly blocked paths before continuing.

claim.updateWorkingSet({ paths, mode?, resource?, leaseSeconds? })

Advanced file-aware coordination. Declare the files this active run is editing or reviewing. Use resource: "repo:owner/repo" when multiple project-level claims should coordinate on the same repo paths.

work.finish(claimId, { outcome?, reason?, metadata? })

Releases the claim after success, failure, or no-op. Repeated finish calls are safe through the run API.

Runtime outcomes

What to handle

proceed

The agent may run. In enforce mode this usually includes a claim that should be extended or finished.

skip_run

Another active claim owns the same resource. Exit cleanly and report the reason.

observe mode

The agent proceeds with claim: null. Availsync records whether it would have blocked the run.

finishError

If work succeeds but final cleanup fails, withClaim returns the successful result with finishError instead of throwing away the result.

autoExtendError

If automatic lease extension fails while work continues, withClaim returns the successful result with autoExtendError so cleanup issues stay visible.

AvailsyncError

Auth, validation, plan-limit, network, and unexpected API failures throw a typed error without exposing secrets.

Package status

The public SDK installs with npm install @availsync/node. Pin an exact version for production automations when you need repeatable installs across CI runs.