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.
| Variable | Value | Where it comes from |
|---|---|---|
NSTACK_BASE | https://{instance}.nstack.ai | Your 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_SESSION | A session token | Your app's Clerk SDK, signed in as a member. It lasts about a minute, so fetch it right before you call. |
NSTACK_MACHINE_TOKEN | A machine token | Studio, 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.
| Access | Who grants it | Where in Studio | Used for |
|---|---|---|---|
| Member account | Anyone who can invite members | Members | Every REST call on this page. Reading runs needs the Admin role. |
| Machine token | An admin, one per environment | Settings, Machine access | Starting an automation from your system. |
| MCP key Preview | Anyone who can create or update agents, per MCP config | MCP | An 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.
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.
- List environments and keep the
env_idof the one to read. DEV for a first try. - List datasources, then the datasets of one.
- Read records of a dataset in that environment. Without
limityou get the first 100 rows.
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])
{
"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
}
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.
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"]
{
"correlation_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"run_id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d",
"agent_config_id": "7e8f9a0b-1c2d-4e3f-9a4b-5c6d7e8f9a0b"
}
| Field | Type | Meaning |
|---|---|---|
message | string | What you ask the agent. |
env_id | UUID, optional | The environment to run in. The run uses the version pinned there, else the latest version, else the saved draft. |
thread_id | string, optional | Continues an earlier conversation with the same id. |
execution_report | boolean | True by default. Adds an execution summary to the finished run. |
The read agents permission, which the Member role holds.
| Status | Body | When |
|---|---|---|
| 401 | {"detail": "Missing Authorization or session cookie"} | No session, or one that does not verify. |
| 403 | org_mismatch | The path names an organization other than your session's active one. |
| 403 | forbidden | Your 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.
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.
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()
{
"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": []
}
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.
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.
{
"status": "accepted",
"correlation_id": "5d6e7f80-91a2-4b3c-8d4e-5f6a7b8c9d0e",
"message": "Workflow execution triggered"
}
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | The machine token is invalid, revoked, or expired. |
| 403 | machine_token_not_accepted | The webhook is not marked secure, so it takes sessions only. |
| 403 | machine_token_not_attached | The token is not attached to this webhook for the environment in env. |
| 409 | conflict | The instance_id already ran. |
| 503 | machine_token_unverified | The 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.