Docs
Dashboard
Guides / AIBeta

Tasks

Save an agent run as a task with variables, an output schema and the session each run starts in, run it again with new values, get its results as JSON, and put it on a cron schedule in your time zone.

A task is an agent run you save to use again. It holds an instruction with %name% variables, an optional output schema, the settings of the run’s own session (a browser, a shell or both) and a model. Run it by hand with new values each time, or put it on a schedule.

Each run is an ordinary agent run, tagged with its task. With an output schema, every run’s result is JSON you can use straight away. Tasks are on every plan; schedules are on Hobby and up.

Save a task

POST /v1/tasks saves a task. This one lists the books of a category on books.toscrape.com, a public site made for practising scraping. %category% is a variable, and output asks for the books as JSON.

curl -X POST https://api.boxline.dev/v1/tasks \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "name": "Books by category",    "instruction": "Open https://books.toscrape.com, open the category %category% and list the books on its first page with their prices.",    "variables": [{"name": "category", "default": "Travel", "description": "A category of the site"}],    "output": {      "type": "object",      "properties": {        "books": {          "type": "array",          "items": {            "type": "object",            "properties": {"title": {"type": "string"}, "price": {"type": "number"}},            "required": ["title", "price"]          }        }      },      "required": ["books"]    }  }'
Response 201 (shape)
{  "id": "task_…",  "name": "Books by category",  "instruction": "Open https://books.toscrape.com, open the category %category% and …",  "variables": [    { "name": "category", "secret": false, "default": "Travel", "description": "A category of the site", "origins": null, "shell": false }  ],  "credentials": [],  "output": { "type": "object", "properties": { "books": { "…": "…" } }, "required": ["books"] },  "session": null,  "allowWithExtensions": false,  "model": null,  "maxSteps": 30,  "maxCostUsd": null,  "notifyOnFailure": null,  "schedule": null,  "lastRunAt": null,  "lastRun": null,  "createdAt": "2026-09-30T10:00:00.000Z",  "updatedAt": "2026-09-30T10:00:00.000Z"}
FieldDefaultDescription
namerequired1 to 100 characters.
instructionrequiredWhat the agent should do, with %name% where a variable goes. Up to 20,000 characters.
variablesnoneUp to 50 inputs, each filled in per run. See Variables below.
credentialsnoneUp to 50 credentials by name, typed as placeholders in every run, scheduled ones too. See Credentials below.
outputnoneA JSON Schema the answer must match, so each run's result is JSON. See Structured output on the AI agent page.
sessiona browser-only sessionThe settings of each run's own session: the same fields as POST /v1/sessions. A browser only, a shell only or both. Checked when you save the task and again at every run. See The session of each run below.
allowWithExtensionsfalseNeeded for a task with secret variables or credentials and a session with extensions, which can read typed values. Without it that is 400 variables_with_extensions.
modelthe platform default{ provider, model } from GET /v1/agent/models, e.g. { "provider": "anthropic", "model": "claude-sonnet-5-5" }.
maxSteps30The most agent steps per run, 1 to 1,000, or null for no step limit.
maxCostUsdnoneEach run's money budget: 0.01 to 100 dollars of model cost, as on an agent run.
notifyOnFailurenull"email" or null. Stored only: no email is sent yet. Use the task_run.finished webhook to hear about failures.
schedulenone{ cron, timezone, variables, enabled }: run the task on its own. See Schedules below.

An Idempotency-Key makes a retried save safe.

The session of each run

session describes the session each run starts in. It takes the same fields as Create a session (a browser, a shell, a proxy, CAPTCHA handling, extensions, a browser profile, a timeout and so on), and an agent run takes the same object. Leave it out for a browser only. A session is a browser only, a shell only or both: see The three kinds of session.

curl -X POST https://api.boxline.dev/v1/tasks \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "name": "Report total",    "instruction": "Download https://example.com/report.csv with curl, add up its revenue column with Python and answer with the total.",    "session": {"browser": false, "shell": true, "timeout": 600}  }'
  • Shell-only tasks work. With browser: false and shell: true the agent has no browser tools: see Shell only.
  • Checked twice. The session is checked when you save the task and again at every run, so an extension or browser profile deleted since then fails the run (404 not_found).
  • Change it as a whole. PATCH replaces the whole session. Send { "session": null } to remove it, and the next run starts a browser-only session.
  • How long. Each run’s session lasts 1,800 seconds unless session.timeout says otherwise, or your plan’s maximum when that is shorter. Above the plan’s maximum is 402 plan_limit, when you save and at each run.
  • Secrets in the session stay hidden. session.env (variables for the shell) is stored sealed, and a task reads back only its names. A custom proxy’s password is stored encrypted and never returned.
  • Two names, two jobs. The task’s credentials are what the AI may type. session.credentials are exported into the session’s shell for commands.
  • allowWithExtensions is a field of the task, not of session. A task with secret variables or credentials and a session with extensions needs allowWithExtensions: true: an extension can read values as they are typed.
  • With sessionId a run works in that session, and the task’s session does not apply.

Variables

A variable is an input you fill in per run. Write it in the instruction as %name%. There are two kinds, and they are handled very differently.

  • Plain variables (the default) are text. Their value, from the run, the schedule or the default, is written into the instruction, so the model reads it. It is stored with the run.
  • Secret variables (secret: true) are for passwords and tokens. The value is never stored anywhere, so it has no default and must come with every run. It goes to the run as an agent variable: the model sees only %name%, and the value is hidden in everything the run keeps.
FieldWhat it does
nameLetters, digits and _, not starting with a digit, up to 64 characters. Written %name% in the instruction.
defaultThe value when a run gives none. Not for secret variables.
descriptionWhat to fill in, up to 500 characters (for you and your team; the model does not see it).
secrettrue: the value is never stored and must come with every run. The model sees only %name%.
originsSecret variables only: the sites whose fields may receive the value, as for agent variables. Recommended for passwords.
shellSecret variables only: true lets shell commands use the value too.

Limits, as for agent variables: up to 50 variables, each value up to 8,000 characters, and 64 KB together.

Credentials, for scheduled sign-ins

credentials names credentials the task’s runs may type, exactly as credentials on an agent run: %NAME% for a secret, %NAME.username%, %NAME.password% and %NAME.otp% for a password, only on each credential’s own origins, into a shell command only with shell: true, and never shown to the model. A password whose codes your system sends (codeSource "push" or "url") makes a scheduled run wait for a code, so your system must answer the credential.code_needed webhook or the codeUrl request at any hour the task runs. See 2FA codes and sign-in links. Your project owns the credential, so a scheduled task may use it. That is the way to sign in on a schedule; secret variables still cannot be scheduled. A task whose session.profile links a password credential gets it too, without listing it.

curl -X POST https://api.boxline.dev/v1/tasks \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "name": "Latest invoice",    "instruction": "Sign in to https://shop.example.com: type %SHOP.username% and %SHOP.password% into the sign-in form, open Billing and read the latest invoice.",    "credentials": ["SHOP"],    "output": {"type": "object", "properties": {"date": {"type": "string"}, "total": {"type": "number"}}, "required": ["date", "total"]},    "schedule": {"cron": "0 8 1 * *", "timezone": "Europe/London"}  }'
  • The task keeps the names only. Each run reads the values when it starts, so a changed value is used from the next run on.
  • When you save the task, each name is checked as on an agent run: an unknown one gets 404 credential_not_found, one with scope "shell" gets 400 credential_not_for_ai, and a name that is also one of the task’s variables gets 400.
  • If a credential is deleted (or given scope "shell") after that, the run fails with that code: a run by hand gets the error and leaves no task run behind, and a scheduled run is kept as failed with that errorCode.
  • PATCH replaces the whole list; null or [] removes it. Each run’s use of a credential is in the audit log, with usedBy: { type: "task_run" }.

Run a task

POST /v1/tasks/:id/runs starts a run with the task’s settings and the values you send. It answers at once with the task run (status running). Its runId is the agent run: follow it with GET /v1/agent/runs/:runId. In the SDKs, bx.tasks.waitForRun(run) (Python: wait_for_run) waits for the run and returns it with its result.

curl -X POST https://api.boxline.dev/v1/tasks/$TASK_ID/runs \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{"variables": {"category": "Poetry"}}'# {"id": "trun_…", "runId": "run_…", "status": "running", …}# Follow the agent run. Once it completes, "result" is the JSON answer.curl https://api.boxline.dev/v1/agent/runs/$RUN_ID \  -H "x-api-key: $BOXLINE_API_KEY"
  • variables gives values by name. A variable with a default can be left out. One without a value gets 400 missing_variables, and a name the task does not have gets 400.
  • sessionId runs the task in a session you already have. That session’s own settings apply, and the task’s session does not.
  • A run counts as an agent run: 30 a minute per project together with POST /v1/agent/runs, your plan’s running sessions, and its model spend cap.
  • Watch, pause, resume or cancel a run through its agent run, as on the AI agent page. A run that stopped early can be resumed like any agent run. It is the same run, so the task’s history shows it once, going on.

Results as JSON

With an output schema, a completed run’s result is the JSON answer, checked against the schema. resultText is a one-sentence summary, and structured is true. Without a schema, result is the agent’s final text.

A task run (shape)
{  "id": "trun_…",  "taskId": "task_…",  "runId": "run_…",  "sessionId": "…",  "status": "completed",  "scheduled": false,  "scheduledFor": null,  "reason": null,  "missedCount": null,  "variables": { "category": "Poetry" },  "secretVariables": [],  "structured": true,  "result": { "books": [{ "title": "A Light in the Attic", "price": 51.77 }, "…"] },  "resultText": "Listed the books on the first page of Poetry.",  "error": null,  "errorCode": null,  "usage": { "inputTokens": 5120, "outputTokens": 800, "costUsd": 0.0182 },  "durationMs": 41000,  "createdAt": "2026-09-30T10:00:00.000Z",  "finishedAt": "2026-09-30T10:00:41.000Z"}

An answer that does not match gets one repair try. If that fails too, the run fails with errorCode: "output_invalid". The supported schema keywords and limits are under Structured output.

Run history

GET /v1/tasks/:id/runs lists a task’s runs, newest first: runs by hand, scheduled runs, and scheduled times that were skipped or missed. ?status= takes one or more statuses, comma-separated.

curl "https://api.boxline.dev/v1/tasks/$TASK_ID/runs?status=failed,skipped,missed" \  -H "x-api-key: $BOXLINE_API_KEY"
statusMeaning
queuedA scheduled run waits for its turn to start.
runningThe agent is working.
pausedThe run waits for a person (you paused it, the agent asked for help, or a CAPTCHA).
completedFinished. result holds the answer.
failedThe run failed, or a scheduled run could not start. errorCode says why.
canceledSomeone canceled the agent run.
skippedA scheduled time that did not run. reason says why.
missedScheduled times that passed while the platform could not run them. missedCount says how many.
  • variables has the plain values the run used. secretVariables has the names of its secret variables, never their values.
  • runId is null for skipped and missed times, and for a scheduled run that could not start.
  • Skipped and missed entries are kept 30 days.
  • Deleting a task (DELETE /v1/tasks/:id) deletes its history too. Its agent runs stay, and runs in progress go on.

Schedules

A schedule runs the task on its own: a cron expression, read on the clock of a time zone you choose, with values for its variables. Add one when you save the task, or later with PATCH /v1/tasks/:id. The fields you send are merged into the current schedule, so { "enabled": false } pauses it and null removes it.

curl -X PATCH https://api.boxline.dev/v1/tasks/$TASK_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{    "schedule": {"cron": "0 9 * * MON-FRI", "timezone": "Europe/London", "variables": {"category": "Poetry"}}  }'# "schedule": {"cron": "0 9 * * MON-FRI", …, "enabled": true, "nextRunAt": "2026-10-01T08:00:00.000Z"}# Pause it:curl -X PATCH https://api.boxline.dev/v1/tasks/$TASK_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"schedule": {"enabled": false}}'# Remove it:curl -X PATCH https://api.boxline.dev/v1/tasks/$TASK_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"schedule": null}'
FieldDefaultDescription
cronrequiredWhen to run, as five fields. See Cron below.
timezone"UTC"An IANA time zone such as "Europe/London" or "America/New_York". The cron is read on its clock.
variablesthe defaultsValues for the task’s variables. Every variable needs a value or a default (400 missing_variables).
enabledtrueSwitch the schedule off and on. A schedule that is switched off does not count toward your plan.

The task shows schedule.nextRunAt, the next time it runs (null while switched off), and schedule.nextRuns, the next 3 times in UTC, with the time zone and daylight saving applied ([] while switched off). Switching it on, or changing cron or timezone, counts from now: times in between are not made up.

Cron

Five fields: minute, hour, day of the month, month and day of the week. Each takes *, numbers, ranges (1-5), steps (*/15), lists (1,15) and names (JAN to DEC, SUN to SAT; 0 and 7 are both Sunday).

cronRuns
0 9 * * MON-FRIEvery weekday at 09:00.
*/15 8-18 * * *Every 15 minutes from 08:00 to 18:45.
30 6 1 * *At 06:30 on the 1st of every month.
0 12 * * SUNEvery Sunday at noon.
@dailyEvery day at 00:00 (also @hourly, @weekly, @monthly, @yearly).
  • At most every 5 minutes. A cron that runs more often, such as * * * * * or */2 * * * *, gets 400 invalid_request.
  • When both day fields are set, a day that matches either one runs, as in classic cron: 0 9 1 * MON runs on the 1st and on every Monday.
  • A cron that never runs, such as the 31st of February, is refused.

Daylight saving

The cron follows the time zone’s clock, so the days the clocks change need a rule:

  • A fixed time (the minute and the hour are numbers, not *) that falls in the hour the clocks skip runs when they change, moved forward by the jump. In Europe/Berlin, a task at 02:30 runs at 03:30 on the day the clocks go forward. In a zone that moves by 30 minutes, it moves by 30 minutes.
  • A fixed time in the hour that happens twice, when the clocks go back, runs once.
  • A schedule with * in the minute or hour, such as */15 * * * *, follows real time: none in the skipped hour, and both in the repeated one.

To avoid the question, schedule outside the night hours when clocks change (00:00 to 03:00 in most zones), or use UTC.

When runs start

The platform looks for due schedules about every 15 seconds. A due time becomes a queued run, and queued runs start as machines are free, with projects taking turns, so another project’s busy minute never delays yours. A queued run that has not started within 10 minutes fails with not_started.

Never two at once

A task never runs twice at the same time. A time that comes while a run of the task is still going, or still queued, is skipped: it appears in the history with status: "skipped" and reason: "previous_run_running". That includes runs you started by hand. The next time runs as usual.

Missed times are not made up

When the platform could not run schedules (for example during an outage), times more than 2 minutes late are recorded once, as one missed entry with missedCount, and never run later. The next time runs as usual.

What a scheduled run is

  • It acts for your project like an API key, with no user, and starts its own session with the task’s settings, as a run by hand does.
  • It counts toward your plan’s running sessions and its model spend cap, like any run.
  • Your plan is checked again at every time. If it no longer has schedules, or has fewer than you have switched on, times are skipped with reason: "plan_limit"; the schedules switched on first keep running.
  • While a project is suspended, its schedules pause: nothing runs and nothing is recorded, and the times in between are not made up.

Get results by webhook

Every finished task run sends the task_run.finished webhook: completed, failed or canceled, by hand or on its schedule, and also a scheduled run that could not start (then runId is null). With it, you do not need to poll for scheduled runs. Subscribe an endpoint to it:

curl -X POST https://api.boxline.dev/v1/webhooks \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"url": "https://example.com/webhooks/boxline", "events": ["task_run.finished"]}'# The answer holds "secret": "whsec_…", shown only this once.
task_run.finished (body)
{  "id": "evt_…",  "type": "task_run.finished",  "createdAt": "2026-10-01T08:00:44.000Z",  "projectId": "…",  "data": {    "taskId": "task_…",    "taskName": "Books by category",    "taskRunId": "trun_…",    "runId": "run_…",    "status": "completed",    "scheduled": true,    "scheduledFor": "2026-10-01T08:00:00.000Z",    "result": { "books": [{ "title": "A Light in the Attic", "price": 51.77 }, "…"] },    "resultText": "Listed the books on the first page of Poetry.",    "structured": true,    "error": null,    "errorCode": null,    "usage": { "inputTokens": 5120, "outputTokens": 800, "costUsd": 0.0182 },    "variables": ["category"],    "createdAt": "2026-10-01T08:00:03.000Z",    "finishedAt": "2026-10-01T08:00:44.000Z"  }}
  • variables lists names only, never values, not even plain ones. The run history has the plain values.
  • result is at most 32,000 characters of text, or the JSON answer. A JSON answer longer than that is null, with truncated: true: get it from the run history.
  • The agent run sends its own agent_run.finished as well.
  • Verify the signature of every delivery before you trust it. See Verify every delivery.
  • A complete, tested receiver: the Watch a price example, with Visual uptime check and Changelog watcher for more scheduled tasks.

Change and delete tasks

GET /v1/tasks lists your tasks, newest first, and GET /v1/tasks/:id returns one. Each has lastRun: { id, status, createdAt, finishedAt } of its newest run, by hand or scheduled (skipped and missed times are not runs), or null before the first. PATCH /v1/tasks/:id changes the fields you send; null removes output, session, model, maxCostUsd, notifyOnFailure or schedule, and maxSteps: null means no step limit. DELETE /v1/tasks/:id deletes a task and its history. On the command line, boxline tasks list lists your tasks with their schedules.

curl "https://api.boxline.dev/v1/tasks?limit=20" -H "x-api-key: $BOXLINE_API_KEY"curl -X PATCH https://api.boxline.dev/v1/tasks/$TASK_ID \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"maxSteps": 20, "model": {"provider": "anthropic", "model": "claude-sonnet-5-5"}}'curl -X DELETE https://api.boxline.dev/v1/tasks/$TASK_ID -H "x-api-key: $BOXLINE_API_KEY"

Plan limits

LimitFreeHobbyStartupScale
Tasks per project11,000 at most1,000 at most1,000 at most
Schedules switched onnone550no limit
  • Your plan’s values are in GET /v1/auth/me as project.limits.tasks and project.limits.schedules. On paid plans tasks is null (no plan limit), and a project still has at most 1,000 tasks. See Plans and features.
  • Beyond them, saving a task or switching a schedule on gets 402 plan_limit. A schedule that is switched off does not count.
  • Tasks need the plan’s agent runs, which every plan has.
  • Saving, changing and deleting tasks: 60 a minute per project. See Rate limits.

Use tasks safely

  • A schedule spends without you. Every scheduled run uses model tokens and session time. Set maxSteps to what the task needs, keep your plan’s monthly spend cap, and pause schedules you no longer need.
  • Watch for failures. notifyOnFailure sends no email yet. Subscribe to task_run.finished, or check the history with ?status=failed,skipped,missed.
  • Secrets stay secret only as secrets. Plain variables, defaults and the instruction are stored as text. Secret variables are never stored; credentials are stored encrypted and the task keeps only their names. Neither is sent in webhooks. Give a secret variable origins, so it is typed only on its own site (a password credential always has them).
  • Page text is not your instruction. A page can try to steer the agent. Give the task only the credentials and settings it needs, and treat its result as data from the web.

Errors

StatusCodeWhen
400invalid_requestA field is not valid, and the message names it: an unknown field, a session setting outside session, a cron that is not five fields or runs more often than every 5 minutes, an unknown time zone, an output that is not a valid JSON Schema, a secret variable with a default, a schedule on a task with secret variables, or a run value for a variable the task does not have.
400missing_variablesA run, or a schedule, has no value for a variable without a default. The message names them, e.g. %category%.
400variables_with_extensionsA task with secret variables or credentials and extensions in its session, without allowWithExtensions: true.
400credential_not_for_aiA credential in credentials has scope "shell", which the AI may not use.
402plan_limitYour project has as many tasks, or schedules switched on, as its plan allows. See Plan limits below.
402feature_not_in_planYour plan lacks a feature the task needs: agent runs, or a setting in session (a shell, a proxy kind, CAPTCHA solving, browser mode, extensions, browser profiles), or a password credential on a plan without loginDetails.
402spend_limitStarting a run: this month's model spend cap is used up.
404not_foundNo such task, or a browser profile or extension that is not your project's.
404credential_not_foundA name in credentials is not one of your project's credentials: when you save the task, or when you run it after the credential was deleted.
429rate_limitedTask changes: 60 a minute. Runs: 30 a minute, shared with POST /v1/agent/runs.
429 / 503concurrency_limit, no_capacityStarting a run: no session could start now. For no_capacity, a start has already waited up to 110 seconds for a machine. Try again later.
503provider_unavailableStarting a run: the task's model provider is not set up on the platform.

The SDKs raise MissingVariablesError and PlanLimitError for the two task-specific codes, and their usual error classes for the rest.

Why a run failed

A failed task run has an errorCode, in the history and in task_run.finished. It is not an HTTP error: the run was accepted, then failed.

errorCodeMeaning
output_invalidThe answer did not match the output schema, even after the repair try. error lists the problems.
not_startedA scheduled run did not start within 10 minutes of its time (the platform was busy), or its server stopped first. It is not tried again.
schedule_invalidThe schedule could no longer be read, for example a time zone that no longer exists. The schedule was switched off: fix it with PATCH.
missing_variablesA scheduled run had no value for one of the task's variables.
credential_not_found, credential_not_for_aiA credential the task names was deleted, or given scope "shell", after the task was saved.
no_capacity, concurrency_limit, spend_limit, provider_unavailable, feature_not_in_plan, not_foundA scheduled run could not start, for that reason (not_found: a browser profile or extension was deleted).
spend_limit, captcha_timeout, handover_timeout, machine_unreachableThe error that stopped the agent run once it had started, as on the agent run.
account_recoveredA queued scheduled run was canceled because the account's password was reset before its email was confirmed. That also switched the task's schedule off.
internalAn error on the platform's side; error says only that. It was not tried again.

Why a time was skipped

reasonMeaning
previous_run_runningA run of the task (by hand or scheduled) was still going, or still queued.
plan_limitYour plan has no schedules, or fewer than you have switched on.
not_runningWith status missed: the platform was not running schedules at that time.

The full requests and responses are in the API reference, starting at Save a task.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.