DocumentationThreads and runs
Your assistants
Threads and runs
A thread holds a conversation with an assistant. Each question in it starts a run, which you track through to the response.
One conversation turn
Open a thread
POST /v1/threadWith the assistant’s ID.Ask the question
POST /v1/thread/{id}/messagesThe user’s message.Start the run
POST /v1/thread/{id}/runsThe assistant works on this message. The response carries the run ID.
# Open a thread with the assistant.
curl https://api.learnya.ai/v1/thread \
-H "Authorization: Bearer $LEARNYA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"assistantId": "ASSISTANT_ID", "title": "Ticket 42"}'
# Post the question.
curl https://api.learnya.ai/v1/thread/THREAD_ID/messages \
-H "Authorization: Bearer $LEARNYA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Where is my invoice?"}'
# Start the run on it. The answer carries the run id.
curl https://api.learnya.ai/v1/thread/THREAD_ID/runs \
-H "Authorization: Bearer $LEARNYA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"triggerMessageId": "MESSAGE_ID"}'def post(path, body):
reply = requests.post(
f"{API}{path}", headers=HEADERS, json=body
)
reply.raise_for_status()
return reply.json()
thread = post("/thread", {"assistantId": assistant["id"]})
question = post(
f"/thread/{thread['id']}/messages",
{"content": "Where is my invoice?"},
)
run = post(
f"/thread/{thread['id']}/runs",
{"triggerMessageId": question["id"]},
)
run_id = run["id"]async function post(path, body) {
const r = await fetch(`${API}${path}`, {
method: "POST",
headers,
body: JSON.stringify(body),
})
if (!r.ok) throw new Error(`${r.status} on ${path}`)
return r.json()
}
const thread = await post("/thread", {
assistantId: assistant.id,
})
const question = await post(`/thread/${thread.id}/messages`, {
content: "Where is my invoice?",
})
const run = await post(`/thread/${thread.id}/runs`, {
triggerMessageId: question.id,
})
const runId = run.idasync function post<T>(path: string, body: unknown) {
const r = await fetch(`${API}${path}`, {
method: "POST",
headers,
body: JSON.stringify(body),
})
if (!r.ok) throw new Error(`${r.status} on ${path}`)
return (await r.json()) as T
}
const thread = await post<{ id: string }>("/thread", {
assistantId: assistant.id,
})
const question = await post<{ id: string }>(
`/thread/${thread.id}/messages`,
{ content: "Where is my invoice?" },
)
const run = await post<{ id: string; status: string }>(
`/thread/${thread.id}/runs`,
{ triggerMessageId: question.id },
)
const runId = run.idTrack the run
Poll the run until it ends. Once it has completed, the response is in output.
| Status | What it means |
|---|---|
queued | Waiting for a free worker |
in_progress | The assistant is working |
completed | Finished, the response is ready |
failed | Stopped by an error, described in error |
canceled | Stopped before completion |
import time
while True:
reply = requests.get(f"{API}/run/{run_id}", headers=HEADERS)
run = reply.json()
if run["status"] in ("completed", "failed", "canceled"):
break
time.sleep(1)
if run["status"] == "completed":
print(run["output"][0]["text"])
else:
print(run["status"], run.get("error"))const DONE = ["completed", "failed", "canceled"]
let run
do {
await new Promise((done) => setTimeout(done, 1000))
run = await fetch(`${API}/run/${runId}`, { headers }).then(
(r) => r.json(),
)
} while (!DONE.includes(run.status))
const done = run.status === "completed"
console.log(done ? run.output[0].text : run.error)type RunStatus =
| "queued"
| "in_progress"
| "completed"
| "failed"
| "canceled"
interface Run {
id: string
status: RunStatus
output?: { type: "text"; text: string }[] | null
error?: string | null
}
const DONE: RunStatus[] = ["completed", "failed", "canceled"]
let run: Run
do {
await new Promise((done) => setTimeout(done, 1000))
const r = await fetch(`${API}/run/${runId}`, { headers })
run = (await r.json()) as Run
} while (!DONE.includes(run.status))
const done = run.status === "completed"
console.log(done ? run.output?.[0].text : run.error)Response
{
"id": "c2a7e9d4-1b3f-4e6a-8c5d-9f0b2e4a6d18",
"threadId": "8e4b1d6f-2c9a-4f7e-b3d5-0a6c8e2f4b91",
"assistantId": "6f1c2b0e-3a4d-4c8e-9b7a-2d5e8f1a0c34",
"status": "completed",
"output": [
{ "type": "text", "text": "Your invoice 2026-114 was sent on 2 October." }
],
"modelUsed": "learnya-flash",
"error": null
}Also available
GET /v1/thread/{id}/messages The thread’s messages, up to 400 per page.
GET /v1/run/{id}/stream The response as server-sent events, while it is being written.
POST /v1/runs/{id}/stop Stops a run.
GET /v1/threads The user’s threads, filterable by assistant.
Configure a run
modelOverridetext- The model for this run only: flash or max.
reasoning_efforttext- Reasoning effort: none, low, medium or high.
maxStepsinteger- The maximum number of steps in the run, from 1 to 500.
Limits
- 20 runs per minute per user
- 600 requests per minute for the other routes
- The number of runs per day depends on the workspace’s plan