Quickstart

Six calls from sign-in to a run record. Read your records, run an agent, see what it did step by step, then start an automation from your own system with a machine token.

Time
About 15 minutes
Needs
A member account, an agent, a dataset
Examples
curl · TypeScript · Python

01Before you start

The examples read four values from your shell or process environment.

VariableValueWhere it comes from
NSTACK_BASEhttps://{instance}.nstack.aiYour instance's domain, from your NSTACK team.
NSTACK_ORG{org_id}, which starts with org_The first call below returns it, as active_org_id.
NSTACK_SESSIONA session tokenYour app's Clerk SDK, signed in as a member. It lasts about a minute, so fetch it right before you call.
NSTACK_MACHINE_TOKENA machine tokenStudio, Settings, Machine access. Only the last step uses it.

TypeScript uses fetch on Node 18 or later; Python uses requests. Nothing to install: these are plain HTTP calls.

Quickstart · FlowThe calls on this page in order, and the run record they leave behind.

02Get access in Studio

Someone in your organization grants each kind of access in Studio.

AccessWho grants itWhere in StudioUsed for
Member accountAnyone who can invite membersMembersEvery REST call on this page. Reading runs needs the Admin role.
Machine tokenAn admin, one per environmentSettings, Machine accessStarting an automation from your system.
MCP key PreviewAnyone who can create or update agents, per MCP configMCPAn outside AI assistant calling your agents as tools.

03Make the first call

Ask who you are. Any member can make this call, and it returns your active organization.

GET/core/api/v1/organizations/{org_id}/me

export NSTACK_BASE="https://{instance}.nstack.ai"
export NSTACK_ORG="{org_id}"

curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/me" \
  -H "Authorization: Bearer $NSTACK_SESSION"

const base = process.env.NSTACK_BASE;
const org = process.env.NSTACK_ORG;
const auth = { Authorization: `Bearer ${process.env.NSTACK_SESSION}` };

const me = await (await fetch(`${base}/core/api/v1/organizations/${org}/me`, { headers: auth })).json();
console.log(me.active_org_id, me.org.role);

import os
import requests

base = os.environ["NSTACK_BASE"]
org = os.environ["NSTACK_ORG"]
auth = {"Authorization": f"Bearer {os.environ['NSTACK_SESSION']}"}

me = requests.get(f"{base}/core/api/v1/organizations/{org}/me", headers=auth).json()
print(me["active_org_id"], me["org"]["role"])

A 403 org_mismatch means the path names an organization other than your session's active one. See organization scoping.

04List datasets and records

Your records live in datasets, and a dataset belongs to a connection, called a datasource in the API. Records are read per environment, so pick one first.

  1. List environments and keep the env_id of the one to read. DEV for a first try.
  2. List datasources, then the datasets of one.
  3. Read records of a dataset in that environment. Without limit you get the first 100 rows.
GET…/datasources/{datasource_id}/datasets/{dataset_id}/get-data

H="Authorization: Bearer $NSTACK_SESSION"
ORG="$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG"
DATA="$NSTACK_BASE/data/api/v1/organizations/$NSTACK_ORG"

curl -s "$ORG/environments" -H "$H"
curl -s "$DATA/datasources" -H "$H"
curl -s "$DATA/datasources/$DATASOURCE_ID/datasets" -H "$H"
curl -s "$DATA/datasources/$DATASOURCE_ID/datasets/$DATASET_ID/get-data?env_id=$ENV_ID&limit=20" -H "$H"

const data = `${base}/data/api/v1/organizations/${org}`;
const get = async (url: string) => (await fetch(url, { headers: auth })).json();

const envs = await get(`${base}/core/api/v1/organizations/${org}/environments`);
const sources = await get(`${data}/datasources`);
const datasets = await get(`${data}/datasources/${datasourceId}/datasets`);

const page = await get(`${data}/datasources/${datasourceId}/datasets/${datasetId}/get-data?env_id=${envId}&limit=20`);
console.log(page.original.row_count, page.original.rows[0]);

data = f"{base}/data/api/v1/organizations/{org}"

envs = requests.get(f"{base}/core/api/v1/organizations/{org}/environments", headers=auth).json()
sources = requests.get(f"{data}/datasources", headers=auth).json()
datasets = requests.get(f"{data}/datasources/{datasource_id}/datasets", headers=auth).json()

page = requests.get(f"{data}/datasources/{datasource_id}/datasets/{dataset_id}/get-data",
                    params={"env_id": env_id, "limit": 20}, headers=auth).json()
print(page["original"]["row_count"], page["original"]["rows"][0])
Responseget-data · 200 · sample values

{
  "connector": "pgsql",
  "datasource_id": "3f9a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c",
  "dataset_id": "6c7d8e9f-0a1b-4c2d-9e3f-4a5b6c7d8e9f",
  "env_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
  "query": { "…": "…" },
  "original": {
    "ok": true,
    "rows": [{ "id": 1, "status": "open", "updated_at": "2026-09-30T16:04:00Z" }],
    "row_count": 1,
    "columns": [{ "…": "…" }],
    "page": { "…": "…" }
  },
  "mapped": null
}
Requires

The read permissions the Member role holds. Lists answer {"items": […], "next_cursor": …}.

05Run an agent

Send a message to an agent in one environment. The call queues the run and answers at once with its ids; the agent works after the response.

POST/agents/api/v1/organizations/{org_id}/agents-config/{agent_config_id}/test

curl -s -X POST "$NSTACK_BASE/agents/api/v1/organizations/$NSTACK_ORG/agents-config/$AGENT_CONFIG_ID/test" \
  -H "Authorization: Bearer $NSTACK_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"message":"Which records changed this week?","env_id":"'"$ENV_ID"'"}'

const res = await fetch(`${base}/agents/api/v1/organizations/${org}/agents-config/${agentConfigId}/test`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ message: "Which records changed this week?", env_id: envId }),
});
const { run_id } = await res.json();

res = requests.post(f"{base}/agents/api/v1/organizations/{org}/agents-config/{agent_config_id}/test",
                    json={"message": "Which records changed this week?", "env_id": env_id}, headers=auth)
run_id = res.json()["run_id"]
Response200 · sample values

{
  "correlation_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
  "run_id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d",
  "agent_config_id": "7e8f9a0b-1c2d-4e3f-9a4b-5c6d7e8f9a0b"
}
FieldTypeMeaning
messagestringWhat you ask the agent.
env_idUUID, optionalThe environment to run in. The run uses the version pinned there, else the latest version, else the saved draft.
thread_idstring, optionalContinues an earlier conversation with the same id.
execution_reportbooleanTrue by default. Adds an execution summary to the finished run.
Requires

The read agents permission, which the Member role holds.

StatusBodyWhen
401{"detail": "Missing Authorization or session cookie"}No session, or one that does not verify.
403org_mismatchThe path names an organization other than your session's active one.
403forbiddenYour role lacks the read agents permission.
422{"detail": […]}The body does not validate, such as an env_id that is not a UUID.

The route answers before the agent starts, so a problem found later ends the run as failed with the reason on its record.

Mode

Agents run in Reads and drafts mode today. Their built-in tools, query_<dataset> and search_knowledge, read your records and knowledge. The answer is a draft that a person acts on.

06Read its run record

Every run lands on the run record: a header, its steps, and its events in order. Each tool call is its own step, named tool:{name}#{n}, so you can see exactly what the agent read.

GET/core/api/v1/organizations/{org_id}/runs/{run_id}

RUNS="$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/runs"

curl -s "$RUNS/$RUN_ID" -H "Authorization: Bearer $NSTACK_SESSION"
curl -s "$RUNS/$RUN_ID/events?after_seq=0" -H "Authorization: Bearer $NSTACK_SESSION"

const runs = `${base}/core/api/v1/organizations/${org}/runs`;

const { run, steps } = await (await fetch(`${runs}/${run_id}`, { headers: auth })).json();
console.log(run.status, steps.map((s) => `${s.name}:${s.status}`).join(" "));

const events = await (await fetch(`${runs}/${run_id}/events?after_seq=0`, { headers: auth })).json();

runs = f"{base}/core/api/v1/organizations/{org}/runs"

record = requests.get(f"{runs}/{run_id}", headers=auth).json()
print(record["run"]["status"], [(s["name"], s["status"]) for s in record["steps"]])

events = requests.get(f"{runs}/{run_id}/events", params={"after_seq": 0}, headers=auth).json()
Response200 · trimmed · sample values

{
  "run": {
    "run_id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d",
    "kind": "agent_run",
    "title": "Test Research assistant v3",
    "status": "succeeded",
    "actor": { "type": "user", "id": "user_2abc", "label": "Your Name" },
    "producer": "agents",
    "queued_at": "2026-10-02T14:30:01.902Z",
    "ended_at": "2026-10-02T14:30:09.415Z"
  },
  "steps": [
    { "step_key": "context", "name": "context", "kind": "phase", "status": "succeeded" },
    { "step_key": "tools", "name": "tools", "kind": "phase", "status": "succeeded" },
    { "step_key": "tool:search_knowledge#1", "parent_step_key": "tools", "kind": "tool", "status": "succeeded" }
  ],
  "children": []
}
Requires

The runs read permission, which the Admin role holds. Studio's Runs page shows the same record.

07Start an automation from your system

Your system has no member session, so it starts work through a secure webhook. In Studio, mark the automation's webhook secure and attach the environment's machine token. Then call it.

POST/workflow/api/v1/organizations/{org_id}/workflows/{workflow_id}?env={env_id}

curl -s -X POST "$NSTACK_BASE/workflow/api/v1/organizations/$NSTACK_ORG/workflows/$WORKFLOW_ID?env=$ENV_ID" \
  -H "Authorization: Bearer $NSTACK_MACHINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json

await fetch(`${base}/workflow/api/v1/organizations/${org}/workflows/${workflowId}?env=${envId}`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.NSTACK_MACHINE_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});

requests.post(f"{base}/workflow/api/v1/organizations/{org}/workflows/{workflow_id}",
              params={"env": env_id}, json=payload,
              headers={"Authorization": f"Bearer {os.environ['NSTACK_MACHINE_TOKEN']}"})

The body is any JSON object, and the run starts with it as its input. env is required. For a dry run, test=true sets the run up without starting it, and a later call with its instance_id starts it once; the same instance_id never runs twice.

Response200 · sample values

{
  "status": "accepted",
  "correlation_id": "5d6e7f80-91a2-4b3c-8d4e-5f6a7b8c9d0e",
  "message": "Workflow execution triggered"
}
StatusCodeWhen
401unauthorizedThe machine token is invalid, revoked, or expired.
403machine_token_not_acceptedThe webhook is not marked secure, so it takes sessions only.
403machine_token_not_attachedThe token is not attached to this webhook for the environment in env.
409conflictThe instance_id already ran.
503machine_token_unverifiedThe token could not be verified right now. Retry with backoff.

The run records your token as its actor, machine:{token_id}, so the run record shows which system started it. The authentication page lists every error code.

08Next steps