Skip to content

AI Runs

AI features are predefined capabilities (e.g., summarization, analysis, coaching). Each invocation creates a run that tracks execution through steps.

Contract update (2026-04): The POST request body was corrected to match the live API. The input object, agentId, and workspaceId fields are not accepted. Use the messages array instead.

Run Orchestration Walkthrough

  1. List the catalog (GET /ai/features) to discover available feature keys.
  2. Create a run (POST /ai/features/:featureKey/runs) with a messages array containing the user prompt.
  3. Watch the run status via GET /ai/runs/:runId or GET /ai/runs/:runId/steps, and stream incremental events through GET /ai/runs/:runId/stream.
  4. Cancel (POST /ai/runs/:runId/cancel) or retry (POST /ai/runs/:runId/retry) when a run stalls, then capture the final output field once the run completes.
javascript
const orchestrateFeature = async () => {
  const featureRes = await fetch(`${BASE_URL}/ai/features`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  });
  const feature = (await featureRes.json()).data.find(
    (f) => f.key === "chain-coach"
  );

  const runRes = await fetch(
    `${BASE_URL}/ai/features/${feature.key}/runs`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        messages: [
          { role: "user", content: "Help me maintain my daily vocabulary streak" }
        ],
      }),
    }
  );
  const run = (await runRes.json()).data;
  const stream = new EventSource(
    `${BASE_URL}/ai/runs/${run.id}/stream`,
    { headers: { Authorization: `Bearer ${TOKEN}` } }
  );
  stream.onmessage = (event) => console.log(JSON.parse(event.data));
};

Endpoints

MethodPathDescriptionAuthRate Limit
GET/ai/featuresList AI feature catalogJWT + Entitlement60/min
POST/ai/features/:featureKey/runsCreate a run for a featureJWT + Entitlement20/min
GET/ai/runsList runs (filterable)JWT + Entitlement60/min
GET/ai/runs/:runIdGet a runJWT + Entitlement60/min
GET/ai/runs/:runId/stepsList steps of a runJWT + Entitlement60/min
GET/ai/runs/:runId/streamSSE stream for a runJWT + Entitlement20/min
POST/ai/runs/:runId/cancelCancel a running runJWT + Entitlement30/min
POST/ai/runs/:runId/retryRetry a failed runJWT + Entitlement10/min

GET /ai/features

List all available AI features in the catalog.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 60/min

Request

No path or query parameters. No request body.

Response

Response Example
json
{
  "data": [
    {
      "key": "chain-coach",
      "name": "Chain Coach",
      "description": "AI-powered coaching for maintaining chain streaks",
      "category": "productivity",
      "inputSchema": {
        "chainId": "string",
        "context": "string"
      }
    },
    {
      "key": "bit-decomposer",
      "name": "Bit Decomposer",
      "description": "Break down complex bits into smaller actionable items",
      "category": "productivity",
      "inputSchema": {
        "bitId": "string"
      }
    }
  ]
}
Response Fields
FieldTypeDescription
keystringFeature identifier
namestringDisplay name
descriptionstringFeature description
categorystringFeature category
inputSchemaobjectExpected input fields

Code Examples

bash
curl https://api.chainabit.com/api/v1/ai/features \
  -H "Authorization: Bearer $TOKEN"
javascript
const res = await fetch(`${BASE_URL}/ai/features`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await res.json();
python
import requests

res = requests.get(
    f"{BASE_URL}/ai/features",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]

POST /ai/features/:featureKey/runs

Create a new run for the specified AI feature.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 20/min

Subscription required. Creating AI feature runs requires an active paid subscription. Credit balance alone is insufficient. Requests without a valid paid subscription receive 403 Forbidden with { "code": "subscription_inactive" }.

Request

FieldTypeRequiredConstraintsDescription
messagesarrayYes1–50 itemsConversation messages to send
messages[].rolestringYes"user" or "assistant"Role of the message author
messages[].contentstringYesNon-emptyMessage content
temperaturenumberNo0–2Sampling temperature
maxOutputTokensnumberNo1–200,000Maximum tokens in the response
idempotencyKeystringNoMax 256 charsPrevents duplicate runs on retry
modelIdstringNoUUIDOverride the default model

Response

Response Example
json
{
  "data": {
    "id": "cm5run001",
    "featureKey": "chain-coach",
    "status": "queued",
    "output": null,
    "startedAt": null,
    "completedAt": null,
    "createdAt": "2026-03-17T10:00:00.000Z"
  }
}
Response Fields
FieldTypeDescription
idstringRun ID
featureKeystringFeature that triggered the run
statusstringqueued, running, completed, failed, cancelled
outputobject | nullRun output (null while running)
startedAtstring | nullISO 8601
completedAtstring | nullISO 8601 or null
createdAtstringISO 8601

Code Examples

bash
curl -X POST https://api.chainabit.com/api/v1/ai/features/chain-coach/runs \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      { "role": "user", "content": "I have been struggling to maintain my daily vocabulary practice streak" }
    ]
  }'
javascript
const res = await fetch(`${BASE_URL}/ai/features/chain-coach/runs`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    messages: [
      {
        role: "user",
        content: "I have been struggling to maintain my daily vocabulary practice streak",
      },
    ],
  }),
});
const { data } = await res.json();
python
import requests

res = requests.post(
    f"{BASE_URL}/ai/features/chain-coach/runs",
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/json",
    },
    json={
        "messages": [
            {
                "role": "user",
                "content": "I have been struggling to maintain my daily vocabulary practice streak",
            }
        ]
    },
)
data = res.json()["data"]

GET /ai/runs

List all runs, with optional filters.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 60/min

Request

ParameterTypeRequiredDescription
agentIdstringNoFilter by agent
workspaceIdstringNoFilter by workspace
featureKeystringNoFilter by feature
statusstringNoFilter by status: queued, running, completed, failed, cancelled
limitnumberNoItems per page
offsetnumberNoPagination offset

Response

Response Example
json
{
  "data": [
    {
      "id": "cm5run001",
      "featureKey": "chain-coach",
      "status": "completed",
      "startedAt": "2026-03-17T10:00:00.000Z",
      "completedAt": "2026-03-17T10:00:12.000Z"
    }
  ],
  "meta": {
    "total": 1,
    "limit": 10,
    "offset": 0
  }
}
Response Fields
FieldTypeDescription
idstringRun ID
featureKeystringFeature that triggered the run
statusstringqueued, running, completed, failed, cancelled
startedAtstringISO 8601
completedAtstring | nullISO 8601 or null

Code Examples

bash
curl "https://api.chainabit.com/api/v1/ai/runs?featureKey=chain-coach&status=completed&limit=10" \
  -H "Authorization: Bearer $TOKEN"
javascript
const params = new URLSearchParams({
  featureKey: "chain-coach",
  status: "completed",
  limit: "10",
});
const res = await fetch(`${BASE_URL}/ai/runs?${params}`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data, meta } = await res.json();
python
import requests

res = requests.get(
    f"{BASE_URL}/ai/runs",
    headers={"Authorization": f"Bearer {TOKEN}"},
    params={"featureKey": "chain-coach", "status": "completed", "limit": 10},
)
body = res.json()

GET /ai/runs/:runId

Get details of a single run.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 60/min

Request

Use the id from Create a Run's response (data.id) as $RUN_ID.

Response

Response Example
json
{
  "data": {
    "id": "cm5run001",
    "featureKey": "chain-coach",
    "status": "completed",
    "output": null,
    "startedAt": "2026-03-17T10:00:00.000Z",
    "completedAt": "2026-03-17T10:00:12.000Z",
    "createdAt": "2026-03-17T10:00:00.000Z"
  }
}
Response Fields
FieldTypeDescription
idstringRun ID
featureKeystringFeature that triggered the run
statusstringqueued, running, completed, failed, cancelled
outputobject | nullRun output (null while running)
startedAtstring | nullISO 8601
completedAtstring | nullISO 8601 or null
createdAtstringISO 8601

Code Examples

bash
curl https://api.chainabit.com/api/v1/ai/runs/$RUN_ID \
  -H "Authorization: Bearer $TOKEN"
javascript
const runId = process.env.RUN_ID; // id of the run to fetch

const res = await fetch(`${BASE_URL}/ai/runs/${runId}`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await res.json();
python
import os
import requests

run_id = os.environ["RUN_ID"]  # id of the run to fetch

res = requests.get(
    f"{BASE_URL}/ai/runs/{run_id}",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]

GET /ai/runs/:runId/steps

List all steps executed within a run.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 60/min

Request

Use the id from Create a Run's response (data.id) as $RUN_ID.

Response

Response Example
json
{
  "data": [
    {
      "id": "cm5step01",
      "runId": "cm5run001",
      "type": "llm_call",
      "status": "completed",
      "input": { "prompt": "Analyze streak data..." },
      "output": { "response": "Based on your data..." },
      "tokensUsed": 1250,
      "durationMs": 3400,
      "createdAt": "2026-03-17T10:00:01.000Z"
    }
  ]
}
Response Fields
FieldTypeDescription
idstringStep ID
runIdstringParent run ID
typestringStep type (e.g., llm_call)
statusstringStep status
inputobjectStep input
outputobjectStep output
tokensUsednumberTokens consumed
durationMsnumberExecution duration in ms
createdAtstringISO 8601

Code Examples

bash
curl https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/steps \
  -H "Authorization: Bearer $TOKEN"
javascript
const runId = process.env.RUN_ID; // id of the run to list steps for

const res = await fetch(`${BASE_URL}/ai/runs/${runId}/steps`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await res.json();
python
import os
import requests

run_id = os.environ["RUN_ID"]  # id of the run to list steps for

res = requests.get(
    f"{BASE_URL}/ai/runs/{run_id}/steps",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]

GET /ai/runs/:runId/stream

Connect to a Server-Sent Events stream to receive real-time updates as a run executes.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 20/min

Request

Use the id from Create a Run's response (data.id) as $RUN_ID.

Response

Response Example
event: run.started
data: {"eventId":1,"type":"run.started","runId":"cm5run001","timestamp":"...","payload":{}}

event: message.delta
data: {"eventId":2,"type":"message.delta","runId":"cm5run001","timestamp":"...","payload":{"delta":"Based on your streak data, "}}

event: message.delta
data: {"eventId":3,"type":"message.delta","runId":"cm5run001","timestamp":"...","payload":{"delta":"I recommend starting with smaller "}}

event: message.completed
data: {"eventId":4,"type":"message.completed","runId":"cm5run001","timestamp":"...","payload":{"content":"Based on your streak data, I recommend starting with smaller goals."}}

event: run.settlement.completed
data: {"eventId":5,"type":"run.settlement.completed","runId":"cm5run001","timestamp":"...","payload":{"status":"completed"}}
Response Fields
FieldTypeDescription
eventIdnumberMonotonically increasing event counter for this run
typestringSSE event type — see SSE Streaming reference for the full catalog
runIdstringRun UUID
timestampstringISO 8601 event timestamp
payloadobjectEvent-specific data (e.g. { delta } for message.delta)

Code Examples

bash
curl -N https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/stream \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream"
javascript
// Browser — uses built-in EventSource (no auth header support)
// For Node.js, install: npm install eventsource
import EventSource from 'eventsource';

const runId = process.env.RUN_ID; // id of the run to stream

const es = new EventSource(
  `${BASE_URL}/ai/runs/${runId}/stream`,
  { headers: { Authorization: `Bearer ${TOKEN}` } }
);

es.addEventListener('message.delta', (e) => {
  const { payload } = JSON.parse(e.data);
  process.stdout.write(payload.delta);
});

es.addEventListener('run.settlement.completed', () => es.close());
es.addEventListener('run.failed', (e) => {
  console.error('Run failed:', JSON.parse(e.data));
  es.close();
});
python
import os
import sseclient
import requests
import json

run_id = os.environ["RUN_ID"]  # id of the run to stream

response = requests.get(
    f"{BASE_URL}/ai/runs/{run_id}/stream",
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "text/event-stream",
    },
    stream=True,
)
client = sseclient.SSEClient(response)
for event in client.events():
    if event.event == "message.delta":
        data = json.loads(event.data)
        print(data["payload"]["delta"], end="", flush=True)
    elif event.event == "run.settlement.completed":
        break

POST /ai/runs/:runId/cancel

Cancel a currently running run.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 30/min

Request

Use the id from Create a Run's response (data.id) as $RUN_ID.

Response

Response Example
json
{
  "data": {
    "cancelled": true
  }
}
Response Fields
FieldTypeDescription
cancelledbooleantrue if the run was cancelled; false if the run was not found or belongs to another account

Ownership enforced. You can only cancel runs that belong to your account. Providing a runId that does not exist or belongs to another account returns { "cancelled": false } — the platform does not distinguish between these cases to avoid information disclosure. Cancellation is best-effort; tokens already streamed are not retracted and may still be billed.

Code Examples

bash
curl -X POST https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/cancel \
  -H "Authorization: Bearer $TOKEN"
javascript
const runId = process.env.RUN_ID; // id of the run to cancel

const res = await fetch(`${BASE_URL}/ai/runs/${runId}/cancel`, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await res.json();
python
import os
import requests

run_id = os.environ["RUN_ID"]  # id of the run to cancel

res = requests.post(
    f"{BASE_URL}/ai/runs/{run_id}/cancel",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]

POST /ai/runs/:runId/retry

Retry a failed run, creating a new run from the same input.

Authentication: JWT Bearer token + active AI entitlement required. Rate limit: 10/min

Request

Use the id from Create a Run's response (data.id) as $RUN_ID.

Response

Response Example
json
{
  "data": {
    "id": "cm5run002",
    "featureKey": "chain-coach",
    "status": "queued",
    "retriedFromRunId": "cm5run001",
    "createdAt": "2026-03-17T10:05:00.000Z"
  }
}
Response Fields
FieldTypeDescription
idstringNew run ID
featureKeystringFeature that triggered the run
statusstringqueued, running, completed, failed, cancelled
retriedFromRunIdstringOriginal run ID that was retried
createdAtstringISO 8601

Code Examples

bash
curl -X POST https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/retry \
  -H "Authorization: Bearer $TOKEN"
javascript
const runId = process.env.RUN_ID; // id of the failed run to retry

const res = await fetch(`${BASE_URL}/ai/runs/${runId}/retry`, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await res.json();
python
import os
import requests

run_id = os.environ["RUN_ID"]  # id of the failed run to retry

res = requests.post(
    f"{BASE_URL}/ai/runs/{run_id}/retry",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]

Built with purpose.