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
inputobject,agentId, andworkspaceIdfields are not accepted. Use themessagesarray instead.
Run Orchestration Walkthrough
- List the catalog (
GET /ai/features) to discover available feature keys. - Create a run (
POST /ai/features/:featureKey/runs) with amessagesarray containing the user prompt. - Watch the run status via
GET /ai/runs/:runIdorGET /ai/runs/:runId/steps, and stream incremental events throughGET /ai/runs/:runId/stream. - Cancel (
POST /ai/runs/:runId/cancel) or retry (POST /ai/runs/:runId/retry) when a run stalls, then capture the finaloutputfield once the run completes.
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
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /ai/features | List AI feature catalog | JWT + Entitlement | 60/min |
| POST | /ai/features/:featureKey/runs | Create a run for a feature | JWT + Entitlement | 20/min |
| GET | /ai/runs | List runs (filterable) | JWT + Entitlement | 60/min |
| GET | /ai/runs/:runId | Get a run | JWT + Entitlement | 60/min |
| GET | /ai/runs/:runId/steps | List steps of a run | JWT + Entitlement | 60/min |
| GET | /ai/runs/:runId/stream | SSE stream for a run | JWT + Entitlement | 20/min |
| POST | /ai/runs/:runId/cancel | Cancel a running run | JWT + Entitlement | 30/min |
| POST | /ai/runs/:runId/retry | Retry a failed run | JWT + Entitlement | 10/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
{
"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
| Field | Type | Description |
|---|---|---|
key | string | Feature identifier |
name | string | Display name |
description | string | Feature description |
category | string | Feature category |
inputSchema | object | Expected input fields |
Code Examples
curl https://api.chainabit.com/api/v1/ai/features \
-H "Authorization: Bearer $TOKEN"const res = await fetch(`${BASE_URL}/ai/features`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await res.json();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 Forbiddenwith{ "code": "subscription_inactive" }.
Request
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
messages | array | Yes | 1–50 items | Conversation messages to send |
messages[].role | string | Yes | "user" or "assistant" | Role of the message author |
messages[].content | string | Yes | Non-empty | Message content |
temperature | number | No | 0–2 | Sampling temperature |
maxOutputTokens | number | No | 1–200,000 | Maximum tokens in the response |
idempotencyKey | string | No | Max 256 chars | Prevents duplicate runs on retry |
modelId | string | No | UUID | Override the default model |
Response
Response Example
{
"data": {
"id": "cm5run001",
"featureKey": "chain-coach",
"status": "queued",
"output": null,
"startedAt": null,
"completedAt": null,
"createdAt": "2026-03-17T10:00:00.000Z"
}
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Run ID |
featureKey | string | Feature that triggered the run |
status | string | queued, running, completed, failed, cancelled |
output | object | null | Run output (null while running) |
startedAt | string | null | ISO 8601 |
completedAt | string | null | ISO 8601 or null |
createdAt | string | ISO 8601 |
Code Examples
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" }
]
}'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();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
| Parameter | Type | Required | Description |
|---|---|---|---|
agentId | string | No | Filter by agent |
workspaceId | string | No | Filter by workspace |
featureKey | string | No | Filter by feature |
status | string | No | Filter by status: queued, running, completed, failed, cancelled |
limit | number | No | Items per page |
offset | number | No | Pagination offset |
Response
Response Example
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Run ID |
featureKey | string | Feature that triggered the run |
status | string | queued, running, completed, failed, cancelled |
startedAt | string | ISO 8601 |
completedAt | string | null | ISO 8601 or null |
Code Examples
curl "https://api.chainabit.com/api/v1/ai/runs?featureKey=chain-coach&status=completed&limit=10" \
-H "Authorization: Bearer $TOKEN"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();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
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Run ID |
featureKey | string | Feature that triggered the run |
status | string | queued, running, completed, failed, cancelled |
output | object | null | Run output (null while running) |
startedAt | string | null | ISO 8601 |
completedAt | string | null | ISO 8601 or null |
createdAt | string | ISO 8601 |
Code Examples
curl https://api.chainabit.com/api/v1/ai/runs/$RUN_ID \
-H "Authorization: Bearer $TOKEN"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();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
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Step ID |
runId | string | Parent run ID |
type | string | Step type (e.g., llm_call) |
status | string | Step status |
input | object | Step input |
output | object | Step output |
tokensUsed | number | Tokens consumed |
durationMs | number | Execution duration in ms |
createdAt | string | ISO 8601 |
Code Examples
curl https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/steps \
-H "Authorization: Bearer $TOKEN"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();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
| Field | Type | Description |
|---|---|---|
eventId | number | Monotonically increasing event counter for this run |
type | string | SSE event type — see SSE Streaming reference for the full catalog |
runId | string | Run UUID |
timestamp | string | ISO 8601 event timestamp |
payload | object | Event-specific data (e.g. { delta } for message.delta) |
Code Examples
curl -N https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"// 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();
});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":
breakPOST /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
{
"data": {
"cancelled": true
}
}Response Fields
| Field | Type | Description |
|---|---|---|
cancelled | boolean | true 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
runIdthat 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
curl -X POST https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/cancel \
-H "Authorization: Bearer $TOKEN"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();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
{
"data": {
"id": "cm5run002",
"featureKey": "chain-coach",
"status": "queued",
"retriedFromRunId": "cm5run001",
"createdAt": "2026-03-17T10:05:00.000Z"
}
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | New run ID |
featureKey | string | Feature that triggered the run |
status | string | queued, running, completed, failed, cancelled |
retriedFromRunId | string | Original run ID that was retried |
createdAt | string | ISO 8601 |
Code Examples
curl -X POST https://api.chainabit.com/api/v1/ai/runs/$RUN_ID/retry \
-H "Authorization: Bearer $TOKEN"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();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"]