Tasks API

Tasks are the unit of execution inside Stagent. Each task moves through explicit lifecycle states and can be executed by any configured agent runtime. The Tasks API supports CRUD operations, fire-and-forget execution, real-time log streaming, and workflow-aware sibling queries.

Quick Start

Create a task, queue it, execute it, and stream the logs — a typical integration flow:

// 1. Create a task — starts in "planned" state
interface Task {
id: string;
title: string;
status: string;
priority: number;
projectId?: string;
assignedAgent?: string;
agentProfile?: string;
}

const taskRes: Response = await fetch("http://localhost:3000/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
  title: "Analyze Q4 revenue trends",
  description: "Review revenue data and produce a summary report with charts",
  projectId: "proj-8f3a-4b2c",
  priority: 1,
  assignedAgent: "claude-code",
  agentProfile: "data-analyst",
}),
});
const task: Task = await taskRes.json();
// → { id: "task-9d4e...", status: "planned", ... }

// 2. Queue the task, then execute it
await fetch(`http://localhost:3000/api/tasks/${task.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "queued" }),
});

const exec: Response = await fetch(`http://localhost:3000/api/tasks/${task.id}/execute`, { method: "POST" });
// → 202 Accepted — agent is now running in the background

// 3. Stream logs in real time
const logs = new EventSource(`http://localhost:3000/api/tasks/${task.id}/logs`);
logs.onmessage = (e: MessageEvent) => {
const entry: { eventType: string; message: string } = JSON.parse(e.data);
console.log(`[${entry.eventType}] ${entry.message}`);
};

// 4. When done, fetch the result
interface TaskOutput {
taskId: string;
status: string;
result: string;
contentType: string;
}
const outputRes: Response = await fetch(`http://localhost:3000/api/tasks/${task.id}/output`);
const output: TaskOutput = await outputRes.json();
// → { taskId: "task-9d4e...", status: "completed", result: "...", contentType: "markdown" }

Base URL

/api/tasks

Endpoints

List Tasks

GET /api/tasks

Retrieve all tasks with optional filtering by project and status. Results are ordered by priority, then newest first.

Query Parameters

Param Type Req Description
projectId string Filter tasks by project UUID
status enum Filter by lifecycle state (e.g. running)

Response 200 — Array of task objects

Task Object

FieldTypeReqDescription
idstring (UUID)*Task identifier
titlestring*Task title
descriptionstringDetailed task brief
projectIdstring (UUID)Associated project
statusenum*Lifecycle state: planned, queued, running, completed, failed, cancelled
prioritynumber (0–3)*0 = P0 Critical, 3 = P3 Low
assignedAgentstringAgent runtime ID
agentProfilestringProfile ID for execution
createdAtISO 8601*Creation timestamp
updatedAtISO 8601*Last modification timestamp

Fetch all running tasks for a specific project — useful for building a dashboard or monitoring view:

// Fetch running tasks for a project
const response: Response = await fetch("http://localhost:3000/api/tasks?projectId=proj-8f3a&status=running");
const tasks: Task[] = await response.json();

console.log(`${tasks.length} running tasks`);
tasks.forEach((t: Task) => console.log(`  [${t.priority}] ${t.title}`));

Example response:

[
  {
    "id": "task-9d4e-a1b2",
    "title": "Analyze Q4 revenue trends",
    "status": "running",
    "priority": 1,
    "projectId": "proj-8f3a-4b2c",
    "assignedAgent": "claude-code",
    "agentProfile": "data-analyst",
    "createdAt": "2026-04-03T10:30:00.000Z",
    "updatedAt": "2026-04-03T10:31:15.000Z"
  }
]

Create Task

POST /api/tasks

Create a new task. The task starts in planned state. Optionally link documents and assign an agent profile.

Request Body

FieldTypeReqDescription
titlestring*Task title (1–200 characters)
descriptionstringTask brief (max 2000 characters)
projectIdstring (UUID)Project to associate with
prioritynumberPriority level 0–3(default: 2)
assignedAgentenumAgent runtime: claude-code, openai-codex-app-server, anthropic-direct, openai-direct, ollama
agentProfilestringProfile ID for agent configuration
documentIdsstring[]Document UUIDs to attach as context

Response 201 Created — The created task object with a generated UUID

Create a task with documents attached — the agent will receive these files as context during execution:

// Create a task with attached documents
const response: Response = await fetch("http://localhost:3000/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
  title: "Summarize meeting notes",
  description: "Extract action items and decisions from the uploaded transcript",
  projectId: "proj-8f3a-4b2c",
  priority: 1,
  assignedAgent: "claude-code",
  agentProfile: "technical-writer",
  documentIds: ["doc-meeting-transcript-001"],
}),
});
const task: Task = await response.json();

console.log(task.id);     // "task-a7c2-..."
console.log(task.status); // "planned"

Errors: 400 — Zod validation failure or runtime/profile incompatibility

{
  "error": {
    "formErrors": [],
    "fieldErrors": {
      "title": ["String must contain at least 1 character(s)"]
    }
  }
}

Get Task

GET /api/tasks/{id}

Retrieve a single task with resolved relationship names and aggregated usage data from the cost ledger.

Response 200 — Task object with enriched fields

Additional Fields (beyond standard task object)

FieldTypeReqDescription
projectNamestringResolved project name
workflowNamestringParent workflow name (if step task)
scheduleNamestringParent schedule name (if scheduled)
usage.inputTokensnumberTotal input tokens consumed
usage.outputTokensnumberTotal output tokens consumed
usage.totalTokensnumberCombined token count
usage.costMicrosnumberTotal cost in microdollars (divide by 1,000,000 for USD)
usage.modelIdstringModel used for execution
usage.startedAtISO 8601Execution start time
usage.finishedAtISO 8601Execution end time

The usage field is only present if the task has execution records in the cost ledger. Use costMicros / 1_000_000 to get USD.

// Fetch task with usage data to check cost
const response: Response = await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2");
const task: Task & { usage?: { costMicros: number; modelId: string; totalTokens: number } } = await response.json();

if (task.usage) {
const costUSD: number = task.usage.costMicros / 1_000_000;
console.log(`Model: ${task.usage.modelId}`);
console.log(`Tokens: ${task.usage.totalTokens.toLocaleString()}`);
console.log(`Cost: $${costUSD.toFixed(4)}`);
}

Example response for a completed task:

{
  "id": "task-9d4e-a1b2",
  "title": "Analyze Q4 revenue trends",
  "status": "completed",
  "projectName": "Marketing Analysis",
  "usage": {
    "inputTokens": 12450,
    "outputTokens": 3200,
    "totalTokens": 15650,
    "costMicros": 48200,
    "modelId": "claude-sonnet-4-6-20250514",
    "startedAt": "2026-04-03T10:31:15.000Z",
    "finishedAt": "2026-04-03T10:32:44.000Z"
  }
}

Errors: 404 — Task not found

Update Task

PATCH /api/tasks/{id}

Update task fields. Status changes are validated against the transition table. Document bindings can be replaced atomically.

Request Body (all fields optional)

FieldTypeReqDescription
titlestringUpdated title (1–200 chars)
descriptionstringUpdated brief (max 2000 chars)
statusenumNew lifecycle state (validated against transitions)
prioritynumberUpdated priority (0–3)
assignedAgentenumNew agent runtime
agentProfilestringNew profile ID
resultstringExecution result text
documentIdsstring[]Replace all document bindings atomically

Promote a planned task to the queue and escalate its priority:

// Escalate and queue a task for immediate execution
await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "queued", priority: 0 }),
});

Errors: 400 — Invalid status transition (e.g. planned → completed) or validation failure, 404 — Not found

Delete Task

DELETE /api/tasks/{id}

Permanently remove a task and its associated data.

Response 200{ "success": true }

// Permanently remove a task
await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2", { method: "DELETE" });

Errors: 404 — Task not found

Execute Task

POST /api/tasks/{id}/execute

Start agent execution. Returns 202 immediately — the agent runs asynchronously in the background. Task must be in queued status.

Response 202 Accepted{ "message": "Execution started" }

The agent runtime is selected from the task’s assignedAgent field. If no profile is set, Stagent auto-classifies one based on the task description. Budget guardrails are enforced before execution begins.

// Fire-and-forget execution — poll status or stream logs to track progress
const res: Response = await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/execute", {
method: "POST",
});

if (res.status === 202) {
console.log("Agent is running — stream /logs to follow progress");
} else if (res.status === 429) {
console.log("Budget exceeded — increase budget in Settings");
}

Errors:

  • 400 — Task not in queued state, or runtime/profile incompatibility
  • 404 — Task not found
  • 429 — Budget limit exceeded

Resume Task

POST /api/tasks/{id}/resume

Resume a failed or cancelled task from its last session checkpoint. Requires a previous execution session. Maximum 3 resumes per task.

Response 202 Accepted{ "message": "Resume started" }

Resume picks up the agent conversation from the last checkpoint, preserving context. If the resume limit (3) is reached, re-queue the task for a fresh start instead.

// Resume a failed task — the agent continues where it left off
const res: Response = await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/resume", {
method: "POST",
});

if (res.status === 400) {
const err: { error: string } = await res.json();
if (err.error.includes("Resume limit")) {
  // Re-queue for fresh execution instead
  await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2", {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ status: "queued" }),
  });
}
}

Errors:

  • 400 — No session to resume, resume limit reached (max 3), or task not in failed/cancelled state
  • 404 — Task not found
  • 429 — Budget limit exceeded

Cancel Task

POST /api/tasks/{id}/cancel

Cancel a running or queued task. Sends a cancellation signal to the active agent runtime.

Response 200{ "success": true }

// Cancel a runaway task
await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/cancel", { method: "POST" });

Errors: 404 — Task not found

Respond to Permission Request

POST /api/tasks/{id}/respond

Allow or deny a pending permission request from an executing agent. Optionally save an always-allow rule so the agent won't ask again.

Request Body

FieldTypeReqDescription
notificationIdstring*ID of the pending notification
behaviorenum*allow or deny
messagestringOptional message back to the agent
updatedInputobjectModified tool input (keys validated against original)
alwaysAllowbooleanSave as permanent permission rule
permissionPatternstringGlob pattern for the always-allow rule

Allow a file write permission and remember the decision for future runs:

// Auto-approve file writes to the reports directory
await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/respond", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
  notificationId: "notif-456",
  behavior: "allow",
  alwaysAllow: true,
  permissionPattern: "write:/reports/*",
}),
});

Response 200{ "success": true }

Errors: 400 — Validation failure or disallowed input keys, 404 — Notification not found, 409 — Already responded

Get Task Output

GET /api/tasks/{id}/output

Retrieve the execution result with auto-detected content type. The contentType field tells you how to render it.

Response Body

FieldTypeReqDescription
taskIdstring*Task identifier
statusstring*Current task status
resultstring*Execution output text
contentTypeenum*Auto-detected: text, markdown, code, json, or unknown
// Fetch output and render based on detected content type
const response: Response = await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/output");
const output: { taskId: string; status: string; result: string; contentType: string } = await response.json();

switch (output.contentType) {
case "markdown": renderMarkdown(output.result); break;
case "json":     renderJSON(JSON.parse(output.result)); break;
case "code":     renderCode(output.result); break;
default:         renderPlainText(output.result);
}

Example response:

{
  "taskId": "task-9d4e-a1b2",
  "status": "completed",
  "result": "## Q4 Revenue Analysis\n\nRevenue grew 23% QoQ...",
  "contentType": "markdown"
}

Errors: 404 — Task not found

Stream Task Logs

SSE /api/tasks/{id}/logs

Real-time Server-Sent Events stream of agent log entries. Polls every 500ms with 15-second keepalive heartbeats.

Responsetext/event-stream with JSON log objects

Each event contains a serialized log entry with eventType, message, and timestamp. The stream stays open until the client disconnects or the task completes.

// Stream agent logs in real time
const source = new EventSource("http://localhost:3000/api/tasks/task-9d4e-a1b2/logs");

source.onmessage = (event: MessageEvent) => {
const log: { eventType: string; message: string; timestamp: string } = JSON.parse(event.data);
const time: string = new Date(log.timestamp).toLocaleTimeString();
console.log(`[${time}] [${log.eventType}] ${log.message}`);
};

source.onerror = () => {
console.log("Stream ended — task may have completed");
source.close();
};

Example stream events:

data: {"eventType":"tool_use","message":"Reading file: src/data/revenue.csv","timestamp":"2026-04-03T10:31:16Z"}

data: {"eventType":"assistant","message":"Analyzing quarterly trends...","timestamp":"2026-04-03T10:31:18Z"}

data: {"eventType":"tool_use","message":"Writing report to /reports/q4-analysis.md","timestamp":"2026-04-03T10:31:22Z"}

Get Task Provenance

GET /api/tasks/{id}/provenance

Retrieve the execution provenance graph — shows how the task was created, what triggered it, and its lineage chain.

Response 200 — Provenance graph object

Provenance traces the origin of a task: was it created manually, spawned by a workflow, triggered by a schedule, or promoted from AI Assist?

// Trace where a task came from
const response: Response = await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/provenance");
const prov: { sourceType: string; parentId: string } = await response.json();

console.log(`Source: ${prov.sourceType}`);  // "workflow", "schedule", "manual"
console.log(`Parent: ${prov.parentId}`);

Errors: 404 — Task not found

Get Sibling Tasks

GET /api/tasks/{id}/siblings

List other tasks in the same workflow run. Returns an empty array if the task is not part of a workflow.

Response Body (Array)

FieldTypeReqDescription
idstring (UUID)*Sibling task ID
titlestring*Task title
statusstring*Current lifecycle state
createdAtISO 8601*Creation timestamp

Useful for building workflow progress views — shows all steps alongside the current task:

// Show workflow progress alongside current task
const response: Response = await fetch("http://localhost:3000/api/tasks/task-9d4e-a1b2/siblings");
const siblings: { id: string; title: string; status: string }[] = await response.json();

siblings.forEach((s) => {
const icon: string = s.status === "completed" ? "✓" : s.status === "running" ? "⟳" : "○";
console.log(`${icon} ${s.title} (${s.status})`);
});

Status Transitions

Tasks follow a strict lifecycle. Invalid transitions return 400.

FromAllowed Targets
plannedqueued, cancelled
queuedrunning, planned, cancelled
runningcompleted, failed, cancelled
completedplanned
failedplanned, queued, running
cancelledplanned, running

Error Format

All validation errors follow the Zod flattened format:

{
  "error": {
    "formErrors": [],
    "fieldErrors": {
      "title": ["String must contain at least 1 character(s)"],
      "priority": ["Number must be less than or equal to 3"]
    }
  }
}

Constants

ConstantValueDescription
MAX_RESUME_COUNT3Maximum resumes per task
DEFAULT_MAX_TURNS50Default agent conversation turns
DEFAULT_MAX_BUDGET_USD2.00Default per-task budget
WORKFLOW_STEP_MAX_BUDGET_USD5.00Budget cap for workflow step tasks