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"] } }'import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst task = await bx.tasks.create({ 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"], },});console.log(task.id); // task_…from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLtask = bx.tasks.create( 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"], },)print(task["id"]) # task_…{ "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"}| Field | Default | Description |
|---|---|---|
name | required | 1 to 100 characters. |
instruction | required | What the agent should do, with %name% where a variable goes. Up to 20,000 characters. |
variables | none | Up to 50 inputs, each filled in per run. See Variables below. |
credentials | none | Up to 50 credentials by name, typed as placeholders in every run, scheduled ones too. See Credentials below. |
output | none | A JSON Schema the answer must match, so each run's result is JSON. See Structured output on the AI agent page. |
session | a browser-only session | The 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. |
allowWithExtensions | false | Needed 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. |
model | the platform default | { provider, model } from GET /v1/agent/models, e.g. { "provider": "anthropic", "model": "claude-sonnet-5-5" }. |
maxSteps | 30 | The most agent steps per run, 1 to 1,000, or null for no step limit. |
maxCostUsd | none | Each run's money budget: 0.01 to 100 dollars of model cost, as on an agent run. |
notifyOnFailure | null | "email" or null. Stored only: no email is sent yet. Use the task_run.finished webhook to hear about failures. |
schedule | none | { 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} }'import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst task = await bx.tasks.create({ 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 },});from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLtask = bx.tasks.create( 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: falseandshell: truethe 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.
PATCHreplaces the wholesession. 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.timeoutsays otherwise, or your plan’s maximum when that is shorter. Above the plan’s maximum is402 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
credentialsare what the AI may type.session.credentialsare exported into the session’s shell for commands. allowWithExtensionsis a field of the task, not ofsession. A task with secret variables or credentials and a session with extensions needsallowWithExtensions: true: an extension can read values as they are typed.- With
sessionIda run works in that session, and the task’ssessiondoes 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.
| Field | What it does |
|---|---|
name | Letters, digits and _, not starting with a digit, up to 64 characters. Written %name% in the instruction. |
default | The value when a run gives none. Not for secret variables. |
description | What to fill in, up to 500 characters (for you and your team; the model does not see it). |
secret | true: the value is never stored and must come with every run. The model sees only %name%. |
origins | Secret variables only: the sites whose fields may receive the value, as for agent variables. Recommended for passwords. |
shell | Secret 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"} }'import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst task = await bx.tasks.create({ 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"], // a password credential, limited to https://shop.example.com by its origins output: { type: "object", properties: { date: { type: "string" }, total: { type: "number" } }, required: ["date", "total"], }, schedule: { cron: "0 8 1 * *", timezone: "Europe/London" }, // 08:00 on the 1st of every month});console.log(task.credentials, task.schedule?.nextRunAt);from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLtask = bx.tasks.create( 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"], # a password credential, limited to https://shop.example.com by its origins output={ "type": "object", "properties": {"date": {"type": "string"}, "total": {"type": "number"}}, "required": ["date", "total"], }, schedule={"cron": "0 8 1 * *", "timezone": "Europe/London"}, # 08:00 on the 1st of every month)print(task["credentials"], task["schedule"]["nextRunAt"])- 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"gets400 credential_not_for_ai, and a name that is also one of the task’s variables gets400. - 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 asfailedwith thaterrorCode. PATCHreplaces the whole list;nullor[]removes it. Each run’s use of a credential is in the audit log, withusedBy: { 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"import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLtype Books = { books: { title: string; price: number }[] };const run = await bx.tasks.run<Books>("TASK_ID", { variables: { category: "Poetry" } });const done = await bx.tasks.waitForRun(run); // polls the run history until it finishesif (done.status === "completed" && done.result) { for (const book of done.result.books) console.log(book.title, book.price);} else { console.log(done.status, done.errorCode, done.error);}from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLrun = bx.tasks.run("TASK_ID", variables={"category": "Poetry"})done = bx.tasks.wait_for_run(run) # polls the run history until it finishesif done["status"] == "completed": for book in done["result"]["books"]: print(book["title"], book["price"])else: print(done["status"], done["errorCode"], done["error"])# Runs the task, waits for it, and prints the result on stdout: JSON with an output schema.boxline tasks run TASK_ID --var category=Poetry > books.json# A secret variable, here %SITE_PASSWORD%, takes its value from the environment variable of that name.SITE_PASSWORD=… boxline tasks run TASK_ID --secret SITE_PASSWORDvariablesgives values by name. A variable with a default can be left out. One without a value gets400 missing_variables, and a name the task does not have gets400.sessionIdruns the task in a session you already have. That session’s own settings apply, and the task’ssessiondoes 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.
{ "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"import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLfor await (const r of bx.tasks.runs("TASK_ID", { status: ["failed", "skipped", "missed"] })) { console.log(r.createdAt, r.status, r.reason ?? r.errorCode, r.scheduledFor);}from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLfor r in bx.tasks.runs("TASK_ID", status=["failed", "skipped", "missed"]): print(r["createdAt"], r["status"], r["reason"] or r["errorCode"], r["scheduledFor"])| status | Meaning |
|---|---|
queued | A scheduled run waits for its turn to start. |
running | The agent is working. |
paused | The run waits for a person (you paused it, the agent asked for help, or a CAPTCHA). |
completed | Finished. result holds the answer. |
failed | The run failed, or a scheduled run could not start. errorCode says why. |
canceled | Someone canceled the agent run. |
skipped | A scheduled time that did not run. reason says why. |
missed | Scheduled times that passed while the platform could not run them. missedCount says how many. |
variableshas the plain values the run used.secretVariableshas the names of its secret variables, never their values.runIdis 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}'import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst task = await bx.tasks.update("TASK_ID", { schedule: { cron: "0 9 * * MON-FRI", timezone: "Europe/London", variables: { category: "Poetry" } },});console.log(task.schedule?.nextRunAt); // when it runs nextawait bx.tasks.update("TASK_ID", { schedule: { enabled: false } }); // pause itawait bx.tasks.update("TASK_ID", { schedule: null }); // remove itfrom boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLtask = bx.tasks.update( "TASK_ID", schedule={"cron": "0 9 * * MON-FRI", "timezone": "Europe/London", "variables": {"category": "Poetry"}},)print(task["schedule"]["nextRunAt"]) # when it runs nextbx.tasks.update("TASK_ID", schedule={"enabled": False}) # pause itbx.tasks.update("TASK_ID", schedule=None) # remove it| Field | Default | Description |
|---|---|---|
cron | required | When 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. |
variables | the defaults | Values for the task’s variables. Every variable needs a value or a default (400 missing_variables). |
enabled | true | Switch 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).
| cron | Runs |
|---|---|
0 9 * * MON-FRI | Every 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 * * SUN | Every Sunday at noon. |
@daily | Every day at 00:00 (also @hourly, @weekly, @monthly, @yearly). |
- At most every 5 minutes. A cron that runs more often, such as
* * * * *or*/2 * * * *, gets400 invalid_request. - When both day fields are set, a day that matches either one runs, as in classic cron:
0 9 1 * MONruns 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. InEurope/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.import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst endpoint = await bx.webhooks.create({ url: "https://example.com/webhooks/boxline", events: ["task_run.finished"],});// endpoint.secret ("whsec_…") is shown only now: put it in your secret manager, never in code or logs.from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLendpoint = bx.webhooks.create("https://example.com/webhooks/boxline", ["task_run.finished"])# endpoint["secret"] ("whsec_…") is shown only now: put it in your secret manager, never in code or logs.{ "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" }}variableslists names only, never values, not even plain ones. The run history has the plain values.resultis at most 32,000 characters of text, or the JSON answer. A JSON answer longer than that isnull, withtruncated: true: get it from the run history.- The agent run sends its own
agent_run.finishedas 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"import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLfor await (const t of bx.tasks.list()) console.log(t.id, t.name, t.schedule?.nextRunAt ?? "no schedule");await bx.tasks.update("TASK_ID", { maxSteps: 20, model: { provider: "anthropic", model: "claude-sonnet-5-5" } });await bx.tasks.delete("TASK_ID"); // its history goes too; its agent runs stayfrom boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLfor t in bx.tasks.list(): print(t["id"], t["name"], (t["schedule"] or {}).get("nextRunAt", "no schedule"))bx.tasks.update("TASK_ID", max_steps=20, model={"provider": "anthropic", "model": "claude-sonnet-5-5"})bx.tasks.delete("TASK_ID") # its history goes too; its agent runs stayPlan limits
| Limit | Free | Hobby | Startup | Scale |
|---|---|---|---|---|
| Tasks per project | 1 | 1,000 at most | 1,000 at most | 1,000 at most |
| Schedules switched on | none | 5 | 50 | no limit |
- Your plan’s values are in
GET /v1/auth/measproject.limits.tasksandproject.limits.schedules. On paid planstasksisnull(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
maxStepsto what the task needs, keep your plan’s monthly spend cap, and pause schedules you no longer need. - Watch for failures.
notifyOnFailuresends no email yet. Subscribe totask_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
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A 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. |
| 400 | missing_variables | A run, or a schedule, has no value for a variable without a default. The message names them, e.g. %category%. |
| 400 | variables_with_extensions | A task with secret variables or credentials and extensions in its session, without allowWithExtensions: true. |
| 400 | credential_not_for_ai | A credential in credentials has scope "shell", which the AI may not use. |
| 402 | plan_limit | Your project has as many tasks, or schedules switched on, as its plan allows. See Plan limits below. |
| 402 | feature_not_in_plan | Your 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. |
| 402 | spend_limit | Starting a run: this month's model spend cap is used up. |
| 404 | not_found | No such task, or a browser profile or extension that is not your project's. |
| 404 | credential_not_found | A 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. |
| 429 | rate_limited | Task changes: 60 a minute. Runs: 30 a minute, shared with POST /v1/agent/runs. |
| 429 / 503 | concurrency_limit, no_capacity | Starting 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. |
| 503 | provider_unavailable | Starting 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.
| errorCode | Meaning |
|---|---|
output_invalid | The answer did not match the output schema, even after the repair try. error lists the problems. |
not_started | A 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_invalid | The 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_variables | A scheduled run had no value for one of the task's variables. |
credential_not_found, credential_not_for_ai | A 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_found | A scheduled run could not start, for that reason (not_found: a browser profile or extension was deleted). |
spend_limit, captcha_timeout, handover_timeout, machine_unreachable | The error that stopped the agent run once it had started, as on the agent run. |
account_recovered | A 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. |
internal | An error on the platform's side; error says only that. It was not tried again. |
Why a time was skipped
| reason | Meaning |
|---|---|
previous_run_running | A run of the task (by hand or scheduled) was still going, or still queued. |
plan_limit | Your plan has no schedules, or fewer than you have switched on. |
not_running | With 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.