Instance API

The Instance API manages the ainative-business process lifecycle on a self-hosted installation. It exposes instance configuration (branch name, data directory, guardrails), idempotent initialization, and the full upgrade detection + application flow. The upgrade system uses a background poller that compares the local branch against origin/main; when commits are available, the operator spawns an upgrade-assistant task that performs a guided git merge.

When running on the canonical ainative-business development repository (AINATIVE_DEV_MODE=true or the .git/ainative-dev-mode sentinel), all endpoints return safe synthetic responses so that dev-environment state never bleeds into production UIs.

Quick Start

Check for available upgrades, trigger the upgrade if one is found, and poll until the task completes:

// 1. Read current instance config and upgrade state
interface InstanceConfig {
branchName: string;
dataDir: string;
createdAt: string;
}
interface UpgradeState {
upgradeAvailable: boolean;
commitsBehind: number;
lastPolledAt: number | null;
lastUpgradeTaskId: string | null;
}
interface ConfigResponse {
devMode: boolean;
config: InstanceConfig | null;
guardrails: object | null;
upgrade: UpgradeState | null;
}

const { devMode, config, upgrade }: ConfigResponse =
await fetch('/api/instance/config').then((r: Response) => r.json());

if (devMode) {
console.log('Running in dev mode — upgrade flow disabled');
} else if (!config) {
// 2. Initialize if not yet bootstrapped
await fetch('/api/instance/init', { method: 'POST' });
console.log('Instance initialized');
} else {
console.log(`Branch: ${config.branchName}`);
console.log(`Upgrade available: ${upgrade?.upgradeAvailable}`);
console.log(`Commits behind: ${upgrade?.commitsBehind}`);
}

// 3. Force-check for new upstream commits
const { ok, state }: { ok: boolean; state?: UpgradeState } =
await fetch('/api/instance/upgrade/check', { method: 'POST' }).then((r: Response) => r.json());

if (ok && state?.upgradeAvailable) {
// 4. Spawn the upgrade task
const { taskId }: { taskId: string } =
  await fetch('/api/instance/upgrade', { method: 'POST' }).then((r: Response) => r.json());

console.log(`Upgrade task spawned: ${taskId}`);
console.log('Navigate to the task view to watch streaming progress');
}

Base URL

/api/instance

Endpoints

Get Instance Config

GET /api/instance/config

Return the full instance state in a single response: config, guardrails, and upgrade state. Used by the Settings → Instance section and the upgrade pre-flight modal. In dev mode returns null payloads to prevent test state from surfacing in the UI.

Response Body

FieldTypeReqDescription
devModeboolean*True when running on the canonical dev repo — all other fields will be null
configobject | null*Instance configuration record
config.branchNamestringGit branch this instance is tracking
config.dataDirstringData directory path
config.createdAtISO 8601Timestamp when the instance was first initialized
guardrailsobject | null*Active guardrail settings (budget caps, tool restrictions)
upgradeUpgradeState | null*Current upgrade state (see Upgrade State schema below)

Read instance state to populate a settings screen or pre-flight modal:

// Read instance state — single round-trip for config + guardrails + upgrade
const res: Response = await fetch('/api/instance/config');
const { devMode, config, guardrails, upgrade }: {
devMode: boolean;
config: { branchName: string; dataDir: string; createdAt: string } | null;
guardrails: object | null;
upgrade: { upgradeAvailable: boolean; commitsBehind: number } | null;
} = await res.json();

if (devMode) {
console.log('Dev mode — instance config unavailable');
} else if (config) {
console.log(`Instance on branch: ${config.branchName}`);
console.log(`Data dir: ${config.dataDir}`);
if (upgrade?.upgradeAvailable) {
  console.log(`${upgrade.commitsBehind} upstream commit(s) available`);
}
}

Example response:

{
  "devMode": false,
  "config": {
    "branchName": "instance/team-prod",
    "dataDir": "/Users/team/.`ainative-business`",
    "createdAt": "2026-03-01T09:00:00.000Z"
  },
  "guardrails": {
    "maxBudgetUsd": 10.00,
    "allowedRuntimes": ["claude-code", "anthropic-direct"]
  },
  "upgrade": {
    "upgradeAvailable": true,
    "commitsBehind": 4,
    "lastPolledAt": 1744800000,
    "lastSuccessfulUpgradeAt": 1743500000,
    "lastUpgradeTaskId": null,
    "pollFailureCount": 0,
    "lastPollError": null
  }
}

Initialize Instance

POST /api/instance/init

Idempotent manual re-run of the instance bootstrap. Useful when the initial boot-time run failed (permission error, git not installed), or when the user wants to re-apply guardrails after changing consent settings. Returns the refreshed instance state after re-running bootstrap.

Response Body

FieldTypeReqDescription
ensureResultobject*Bootstrap result from ensureInstance()
configobject | null*Instance configuration after bootstrap
guardrailsobject | null*Guardrail settings after bootstrap
upgradeobject | null*Upgrade state after bootstrap

Re-run bootstrap to recover from a failed initialization or re-apply changed consent settings:

// Re-run instance bootstrap — safe to call multiple times
const res: Response = await fetch('/api/instance/init', { method: 'POST' });

if (res.ok) {
const { ensureResult, config }: {
  ensureResult: { created: boolean };
  config: { branchName: string } | null;
} = await res.json();

if (ensureResult.created) {
  console.log(`Instance bootstrapped on branch: ${config?.branchName}`);
} else {
  console.log('Instance already initialized — guardrails re-applied');
}
} else {
const { error }: { error: string } = await res.json();
console.error(`Bootstrap failed: ${error}`);
}

Errors: 500 — Bootstrap failure (check logs for permission or git errors)

Spawn Upgrade Task

POST /api/instance/upgrade

Spawn an upgrade-assistant task that performs a guided git merge of upstream commits. Returns 202 Accepted with the task ID immediately — the merge runs asynchronously. Requires upgradeAvailable to be true. Rejects with 409 if the instance is not initialized or no upgrade is available.

Response Body

FieldTypeReqDescription
taskIdstring (UUID)*ID of the spawned upgrade task — navigate to this task to watch progress

The upgrade task uses the upgrade-assistant agent profile. The task description contains the branch name, number of commits behind, and data directory as context variables that the profile’s SKILL.md references. The task runs the standard merge flow: fetch upstream, merge, handle conflicts interactively, and rollback on any failure.

// Spawn the upgrade task — navigate to the task view for streaming progress
const res: Response = await fetch('/api/instance/upgrade', { method: 'POST' });

if (res.status === 202) {
const { taskId }: { taskId: string } = await res.json();
console.log(`Upgrade task: ${taskId}`);
// Navigate to /tasks/${taskId} to watch the merge progress
} else if (res.status === 409) {
const { error }: { error: string } = await res.json();
console.log(`Cannot upgrade: ${error}`);
}

Example response:

{ "taskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }

Errors:

  • 409 — Instance not initialized (call POST /api/instance/init first) or upgradeAvailable is false
  • 500 — Internal failure persisting the task or upgrade state

Check for Upgrades

POST /api/instance/upgrade/check

Force-run the upgrade availability poller. Rate-limited to one run per ~5 minutes via a shared lock file. Compares the local branch HEAD against origin/main using git rev-list. Returns the updated UpgradeState on success, or a skipped reason if the lock was held or dev mode was active.

Response Body (200 — poller ran)

FieldTypeReqDescription
okboolean*True — poller ran successfully
stateUpgradeState*Fresh upgrade state after the poll

Response Body (202 — poller skipped)

FieldTypeReqDescription
okboolean*False — poller was skipped
skippedstringReason: "lock-held", "dev-mode", or "rate-limited"
errorstringError message if the poller threw

Trigger an immediate upgrade check — useful after the user clicks “Check now” in the settings UI:

// Force an upgrade check and display the result
interface UpgradeState {
upgradeAvailable: boolean;
commitsBehind: number;
lastPolledAt: number | null;
pollFailureCount: number;
}

const res: Response = await fetch('/api/instance/upgrade/check', { method: 'POST' });
const body: { ok: boolean; state?: UpgradeState; skipped?: string } = await res.json();

if (body.ok && body.state) {
if (body.state.upgradeAvailable) {
  console.log(`Upgrade available — ${body.state.commitsBehind} commit(s) behind origin/main`);
} else {
  console.log('Up to date');
}
} else {
console.log(`Check skipped: ${body.skipped ?? 'unknown'}`);
}

Example responses:

{
  "ok": true,
  "state": {
    "upgradeAvailable": true,
    "commitsBehind": 4,
    "lastPolledAt": 1744803600,
    "lastSuccessfulUpgradeAt": 1743500000,
    "lastUpgradeTaskId": null,
    "pollFailureCount": 0,
    "lastPollError": null
  }
}
{ "ok": false, "skipped": "rate-limited" }

Errors: 500 — Unexpected poller failure

Get Upgrade Status

GET /api/instance/upgrade/status

Return the current UpgradeState for client components that need to poll (e.g. the upgrade modal pre-flight). In dev mode returns a synthetic state with upgradeAvailable: false so the upgrade button never renders on the dev repo.

Response Body

FieldTypeReqDescription
devModeboolean*True when running on the canonical dev repo
upgradeAvailableboolean*Whether a new upstream version is available
commitsBehindnumber*Number of commits local branch is behind origin/main
lastPolledAtnumber | null*Unix timestamp of the last successful poll
lastSuccessfulUpgradeAtnumber | null*Unix timestamp of the last applied upgrade
lastUpgradeTaskIdstring | null*Task ID of the most recent upgrade task
pollFailureCountnumber*Consecutive poll failures (resets to 0 on success)
lastPollErrorstring | null*Most recent poll error message, if any

Poll upgrade status from a client component — e.g. to show a badge or refresh the pre-flight modal:

// Poll upgrade status from a client component
interface UpgradeStatus {
devMode: boolean;
upgradeAvailable: boolean;
commitsBehind: number;
lastPolledAt: number | null;
lastUpgradeTaskId: string | null;
pollFailureCount: number;
}

const status: UpgradeStatus = await fetch('/api/instance/upgrade/status')
.then((r: Response) => r.json());

if (status.devMode) {
console.log('Dev mode — upgrade status suppressed');
} else if (status.upgradeAvailable) {
console.log(`Upgrade available: ${status.commitsBehind} commits behind`);
if (status.lastUpgradeTaskId) {
  console.log(`Previous upgrade task: ${status.lastUpgradeTaskId}`);
}
} else {
const lastPoll = status.lastPolledAt
  ? new Date(status.lastPolledAt * 1000).toLocaleString()
  : 'never';
console.log(`Up to date. Last checked: ${lastPoll}`);
}

Example response:

{
  "devMode": false,
  "upgradeAvailable": false,
  "commitsBehind": 0,
  "lastPolledAt": 1744803600,
  "lastSuccessfulUpgradeAt": 1744800000,
  "lastUpgradeTaskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "pollFailureCount": 0,
  "lastPollError": null
}

Upgrade State Schema

FieldTypeDescription
upgradeAvailablebooleanTrue when commitsBehind > 0 and last poll succeeded
commitsBehindnumberCommits local branch is behind origin/main
lastPolledAtnumber | nullUnix timestamp of last successful git rev-list check
lastSuccessfulUpgradeAtnumber | nullUnix timestamp of last completed upgrade
lastUpgradeTaskIdstring | nullUUID of the most recently spawned upgrade task
pollFailureCountnumberConsecutive poll failures — resets to 0 on success
lastPollErrorstring | nullError message from the most recent failed poll

Dev Mode

When AINATIVE_DEV_MODE=true is set in the environment or the .git/ainative-dev-mode sentinel file is present, the Instance API operates in dev mode:

  • GET /api/instance/config — returns { devMode: true, config: null, guardrails: null, upgrade: null }
  • GET /api/instance/upgrade/status — returns a synthetic state with upgradeAvailable: false
  • POST /api/instance/upgrade and POST /api/instance/upgrade/check — are not blocked but return early without writing state

This prevents stale instance rows written during local testing from surfacing in production UIs when the dev repo is opened.