Iterant is in early access. These docs describe the product as it works today.
Iterant Docs

Agent tasks

Start one autonomous Terra turn over the API, a free-form instruction or a page build, and poll it to completion.

Agent tasks are the write half of the developer surface. A task is one autonomous Terra turn: you hand it an instruction or an opportunity, it runs in the brand's workspace the way a turn you started in chat would, and you poll a handle until it settles. build_page is the Build-now button, and prompt is a chat message with nobody watching it.

The two commands

prompt takes a free-form instruction in plain language, such as "Add an About page with a team section" or "Rewrite the pricing FAQ for the new tiers". The agent decides which of its own tools it needs. There is no single route at the end of it, so its result is the chat transcript.

build_page takes an opportunity ref from the page-opportunities list and builds that page. It runs the same admission checks as the dashboard's Build-now button, and it is the only command that reports a resultingRoute.

Starting a task

POST /api/v1/public/brands/{brandSlug}/tasks/

curl -X POST https://api.iterant.ai/api/v1/public/brands/acme/tasks/ \
  -H "Authorization: Bearer itr_kK4dQ8v…" \
  -H "Content-Type: application/json" \
  -d '{
        "command": "prompt",
        "prompt": "Add an About page with a team section."
      }'

prompt is required, must be non-blank, and caps at 32,768 characters. A longer brief is refused at creation.

The two argument fields are mutually exclusive. Sending the wrong one is a 400 naming the field you sent: prompt with command: "build_page", or opportunityRef with command: "prompt".

The 202

{
  "taskId": "6f1d8a52-0b3e-4f7a-9c2d-5a1b7e4c8d90",
  "command": "prompt",
  "status": "running",
  "chatUrl": "https://app.iterant.ai/acme/chat?chat=b28c4417-91ad-4de6-8f0c-1c7f2ad50e63",
  "createdAt": "2026-07-26T09:14:03.221Z"
}

The 202 means the work was accepted and is running. You hold a handle to poll and a thread to watch.

taskId is a UUID, and it is the one place on this surface a UUID is the primary handle.

chatUrl deep-links the dashboard thread the turn runs in. For a prompt task that transcript is the result, so put it in the Slack message or the CI summary where a human can read it.

Polling

GET /api/v1/public/brands/{brandSlug}/tasks/{taskId}/

curl https://api.iterant.ai/api/v1/public/brands/acme/tasks/6f1d8a52-0b3e-4f7a-9c2d-5a1b7e4c8d90/ \
  -H "Authorization: Bearer itr_kK4dQ8v…"
{
  "taskId": "6f1d8a52-0b3e-4f7a-9c2d-5a1b7e4c8d90",
  "command": "build_page",
  "status": "succeeded",
  "createdAt": "2026-07-26T09:14:03.221Z",
  "finishedAt": "2026-07-26T09:19:41.008Z",
  "resultingRoute": "/guides/otel-cost-control",
  "chatUrl": "https://app.iterant.ai/acme/chat?chat=…"
}

Poll every 30–60 seconds. A typical task settles in about five minutes.

statusMeaning
runningDispatched and in flight. Keep polling
succeededThe agent finished and reported success
failedThe agent reported failure, or nothing ever came back

There is no queued state. A task is dispatched the moment you create it, so it is running from the first read.

error is present only when status is failed. It is absent otherwise.

A task that has been running for more than two hours with no word from the agent is reported as failed, which frees the brand to start something else. The reap happens when you read the task, so the first poll past the deadline is the one that settles it.

Poll, then submit

The shape of a multi-task integration:

BASE=https://api.iterant.ai/api/v1/public/brands/acme
AUTH="Authorization: Bearer itr_kK4dQ8v…"

for ref in "$@"; do
  task=$(curl -sS -X POST "$BASE/tasks/" -H "$AUTH" \
    -H "Content-Type: application/json" \
    -d "{\"command\":\"build_page\",\"opportunityRef\":\"$ref\"}")

  id=$(echo "$task" | jq -r .taskId)
  echo "started $ref as $id"

  while true; do
    sleep 30
    state=$(curl -sS "$BASE/tasks/$id/" -H "$AUTH")
    [ "$(echo "$state" | jq -r .status)" = "running" ] || break
  done

  echo "$state" | jq -r '"\(.status) \(.resultingRoute // "")"'
done

The loop waits out a 409 and waits out a slow task. Re-POSTing in either case starts a second agent run.

One task at a time, per brand

A create while another task is running for that brand returns the error below. A page build started anywhere else, including in the dashboard, blocks it the same way.

{
  "type": "client_error",
  "errors": [
    {
      "code": "conflict",
      "detail": "A task is already running for this brand. Wait for it to finish, then retry.",
      "attr": null
    }
  ]
}

The agent holds one workspace per brand, so tasks run one after another. That is also why there is no batch endpoint: N items is N tasks submitted in sequence, which is what the loop above does, at a cost of N × several minutes.

Different brands are independent. An orchestrator with an org-scoped key can run one task per brand concurrently.

Listing tasks

GET /api/v1/public/brands/{brandSlug}/tasks/

Newest first, with the standard paging envelope: items, total, offset, limit, note.

curl "https://api.iterant.ai/api/v1/public/brands/acme/tasks/?status=running" \
  -H "Authorization: Bearer itr_kK4dQ8v…"

status filters to running, succeeded, or failed. limit is 1–50, default 20. Each row is the same object the detail route returns, refreshed on read.

?status=running is the cheapest way to check whether this brand is busy before you spend a create on a 409. It is not the whole answer, because a build someone started in the dashboard blocks the brand too and is not a task row. Treat a 409 as the authority.

Callbacks

Pass callbackUrl on create and Iterant POSTs the outcome there when the task settles, either way:

curl -X POST https://api.iterant.ai/api/v1/public/brands/acme/tasks/ \
  -H "Authorization: Bearer itr_kK4dQ8v…" \
  -H "Content-Type: application/json" \
  -d '{
        "command": "build_page",
        "opportunityRef": "otel-cost-control-buyers-guide-3f2a91b4",
        "callbackUrl": "https://hooks.example.com/iterant/tasks"
      }'
{
  "taskId": "6f1d8a52-0b3e-4f7a-9c2d-5a1b7e4c8d90",
  "status": "succeeded",
  "chatUrl": "https://app.iterant.ai/acme/chat?chat=…",
  "resultingRoute": "/guides/otel-cost-control",
  "timestamp": "2026-07-26T09:19:41.008Z"
}

resultingRoute is present only when there is one; error only on failure.

Iterant validates the URL when you create the task. It must be https://, it must not carry user:password@ credentials, and its hostname must resolve to a public address. Loopback, link-local, private, and cloud-metadata ranges are refused with a 400. Redirects are never followed at delivery time.

Errors

429 is not implemented. Nothing throttles this surface yet. The one-task-per-brand guard is a workspace constraint and does not substitute for a rate limit. Rate limiting is planned, and a client that already backs off on 429 will need no change when it arrives.

What a task will not do

  • It will not publish. The agent works in the brand's workspace. Putting a page on a live domain is a separate step in the dashboard, and no task argument changes that.
  • It will not run in bulk. One task per call, one task per brand.
  • It will not write back to the opportunity queue. Dismissing, scheduling, and re-opening an opportunity are dashboard-only.

The same thing over MCP

An agent reaches this surface through two tools: create_agent_task and get_agent_task, with the same commands, the same 409, and the same poll-then-submit instruction written into the tool descriptions. The MCP tool takes no callbackUrl.

MCP tools: create_agent_task, get_agent_task, and the nine reads.

REST API: conventions, the error envelope, and the read endpoints.

Last updated on

On this page