GUIDES
Scheduling tasks
An agent that only acts while someone is watching is half an agent. Let users schedule work and have it run on its own.
Turn scheduling on
Scheduling is a set of reserved tools the agent can call. Enable it in your project settings and four tools become available to the model: schedule_task, list_scheduled_tasks, update_scheduled_task, and cancel_scheduled_task. You do not wire anything up. The agent decides when to use them from what the user asks.
There is a second step, and it is the one people miss. A scheduled run may only use write tools that were pre-authorised for it, and the pool it can draw from is the project's writable tools list, which starts empty. Until an admin adds tools to it in the console, no scheduled task on the project can be given a write action. Enabling scheduling alone gets you tasks that can read, summarise and report, and nothing else.
Project defaults, all editable in settings:
| Field | Type | Description |
|---|---|---|
enabled | false | Off until you turn it on. The reserved tools are not offered to the model while it is off, and due tasks are skipped rather than run. |
writable_tools | [] (empty) | The project-wide pool of write actions a scheduled run may be authorised for. Empty means no scheduled task can write anything. |
max_tasks_per_user | 10 | Counts tasks that are active, paused or running. Done and cancelled tasks do not count. |
min_interval_minutes | 15 | The smallest gap allowed between two fires of a cron task. Measured from the first two occurrences of the expression. |
max_runs_default | 0 | The run cap applied when the agent does not name one. 0 means unlimited. |
DATABASE_URL the task tables do not exist: creating a task returns nothing, listing returns an empty array, and the engine never finds anything due. It is a production feature, not something you will see working on the local SQLite fallback.How users schedule from chat
Users do not fill in a form. They just say what they want, and the agent turns it into a task.
"Every weekday at 9am, check my open orders and message me if any are stuck." "Remind me to review the Q3 numbers next Monday morning." "Run the weekly summary every Friday at 5pm my time, but only for the next month."
Under the hood each of those becomes a scheduled task with a schedule, a timezone, and a run cap.
| Field | Type | Description |
|---|---|---|
One-off | run_at (ISO) | A single run at a specific instant, e.g. 2026-09-01T09:00:00. Must be in the future, or creation is refused. |
Recurring | 5-field cron | Standard cron, e.g. 0 9 * * 1-5 for 9am on weekdays. Exactly one of cron or run_at, never both. |
Timezone | IANA | e.g. Asia/Riyadh. The schedule is evaluated in the user's zone, not the server's. Omitted means UTC. |
max_runs | integer | Cap the number of runs. 0 means unlimited. When the agent does not set one, the project default applies. |
Creation is validated before anything is stored, and each refusal comes back to the model as plain text it relays to the user:
| Field | Type | Description |
|---|---|---|
Too frequent | 15 min | "that schedule runs too often — the minimum interval is 15 minutes". A cron like */5 * * * * is refused at creation. |
Too many tasks | 10 | "you already have the maximum of 10 scheduled tasks — cancel one first". Paused tasks still occupy a slot. |
Bad cron | — | "that recurring schedule isn't a valid cron expression". |
Past time | — | "the one-time run time must be in the future". |
Unauthorised write | — | "these tools can't run unattended on this project: …" when the agent asks for a write tool that is not in the project's writable-tools list. |
How the time and zone are decided
There is no date picker. The agent reads the user's sentence and resolves it — so it needs to know what time it is and where the user is. Agentifys tells it, on every turn:
| Field | Type | Description |
|---|---|---|
The clock | automatic | The current UTC time is injected into the system prompt on every turn. You do not configure this. |
user_context.timezone | signed | Best source. If your backend signs a timezone into the identity, it wins — it is verified. |
client_tz | body field | The browser's zone. The embedded widget sends this automatically. Used only to phrase the prompt, never to authorise anything. |
Neither | — | The agent is told the zone is UNKNOWN and instructed to ask the user before scheduling, rather than assuming UTC. |
Area/City names — Asia/Riyadh, Europe/London, America/New_York. Riyadh, GMT+3 and AST are not valid and are rejected when the task is created. Etc/GMT+3 is rejected too, even though it is a real zone: the POSIX convention inverts its sign, so Etc/GMT+3 means UTC−3, and it ignores daylight saving. UTC and Etc/UTC are accepted. A recurring task stores the zone, so 0 9 * * 1-5 means 9am local all year — daylight saving is handled for you.timezone into your user identity and the question never arises./v1/chat yourself instead of embedding the widget? Send client_tz as a top-level body field. Never add it inside a signed user_context — that breaks the signature.One asymmetry is worth knowing before a user asks for it. A recurring task can be moved to another timezone on its own, because the cron expression is a local time. A one-off task cannot: it was stored as an absolute instant, so the original local time is gone, and the agent is told to ask for a new run_at as well.
What a run actually carries
When a task fires there is no browser open and no user typing. The run is executed as the owning user, with their private knowledge and their vault credentials. It is close to being them, but it is not the same as them being present, and the gap is where the surprises live.
Carried into the run:
| Field | Type | Description |
|---|---|---|
user.id | live | The owning user id, used to scope everything below. |
name / email / role / company | snapshot | Copied from the identity at the moment the task was created, and never refreshed. If the user changes company next month, the run still uses the old value. |
{{user.creds.<name>}} | live | Vault credentials are decrypted fresh on every run, so a tool authenticated with a stored secret behaves exactly as it does in live chat. |
Private knowledge | live | The user:<id> retrieval audience is included, so the run can read that user's own documents alongside the shared pool. |
Timezone | task | The task's own zone becomes the run's clock, so a report that says "today" means today where the user lives. |
{{user.auth_token}} and {{user.data.*}}. The auth token is a live session token — it is stripped before the user record is ever written, and it is not in the task snapshot. In a tool header it resolves to an empty string, so Authorization: Bearer {{user.auth_token}} is sent as Authorization: Bearer and your API sees an unauthenticated call rather than an error. Custom fields are the same story: {{user.data.plan}} is left as literal text in the system prompt and collapses to an empty string in a tool header or parameter. If a tool has to work unattended, authenticate it with a vault credential, not with the session token.user.data.* sees a missing value during a run, and a missing value is not simply "no match": under eq it stringifies to the literal text None, under neq it is therefore true, under gt and lt it reads as 0, and under contains as the empty string. A policy that grants a tool to paying plans will not grant it inside a scheduled run, and one that reads user.data.credits lt 10 fires on every run. Conditions on user.role or user.company still work, because those are in the snapshot.Results do not vanish into a log. A successful run is held and surfaced to the user on their next live turn, so a Friday summary is waiting for them Monday morning. Up to five unseen results are handed to the agent at the start of that turn and marked seen at the same moment — whether or not the agent chooses to mention them. Only runs that finished ok are surfaced this way; a failed run waits in the My-tasks panel instead.
Write actions during a run
Before the model is called, an unattended run is handed a reduced tool list: every tool marked requires confirmation that is not pre-authorised on the task is removed. The model is never offered an action nobody is present to approve, and a pre-authorised write runs straight through with no prompt.
Since the project's writable-tools list starts empty, the practical default is a run with no write tools at all. Nothing errors. The model works with what it has, and finishes with a report saying what it could not do. That run is recorded as a normal, successful one, and the missing write shows up only in the text of the result. Read the result text before assuming a write happened, and check the task's allowed actions if a recurring task keeps producing reports instead of doing the work.
How a run ends
Every attempt writes a run record, and the task's last_run_status and last_error carry the outcome. There are five ways a run ends.
| Field | Type | Description |
|---|---|---|
ok | run status | The agent turn completed. The output is held for the user and the task advances to its next slot. |
error / timeout | run status | A run has a 120-second wall clock. Past it the run is abandoned with last_error "run timed out". Any other exception is recorded the same way, with the message. |
skipped_budget | run status | The project hit its monthly token budget before the run started. Nothing is spent and nothing is executed. last_error is "monthly budget exhausted". |
cancelled | run status | The owning user no longer exists in the project. The run does not happen and the task is cancelled for good, with last_error "user no longer exists". |
blocked | defined | Reserved for a run stopped at an unauthorised write. The current engine removes those tools before the model sees them, so no run is recorded with this status today. |
once task that means status done with no next run. The reminder is gone; raising the budget afterwards does not bring it back. A cron task survives the same gate and simply tries again at its next slot.run_count is incremented after every attempt that reached the agent, including one that timed out or errored, and it is compared against max_runs. A task capped at 5 runs that errors five times is marked done without ever producing a result. Budget skips are the exception — they do not increment the count.Two more things the engine does on its own. A task whose project has scheduling switched off, or that has dropped out of signed identity mode, is passed over on each tick and waits rather than failing. And if the backend dies mid-run, the task is left running until the next startup, which resets it to active.
The My-tasks panel and API
The widget ships a My tasks panel where a user can see their scheduled tasks, what ran, and cancel anything they no longer want. Its calendar button appears on its own once the project is in signed mode with scheduling enabled; you can also open it from your own UI with ERA.openTasks(). The panel is backed by the scheduled-tasks API, which you can also call directly with the user's signed identity in the X-Era-User header.
/v1/scheduled-tasks/v1/scheduled-tasks/seen/v1/scheduled-tasks/{task_id}/v1/scheduled-tasks/{task_id}GET lists a user's tasks and results, POST /seen marks results seen (body {user_context}), PATCH /{task_id} pauses or resumes a task (body {user_context, status:"active"|"paused"}), and DELETE /{task_id} cancels one. The panel is per user, so a person only ever sees and controls their own tasks.
DELETE disappears from GET /v1/scheduled-tasks immediately rather than coming back with status cancelled, so do not poll for that status to confirm the cancellation — treat the task vanishing from the list as the confirmation. The same applies to a task the engine cancelled because its user was deleted. Only the console's admin view shows cancelled tasks.GET /v1/scheduled-tasks returns only the most recent run per task, as last_result truncated to 1000 characters and last_error truncated to 500. Full history — up to 50 runs, each with its tokens, tools used and untruncated output — is available only in the console, under the task in your project's scheduled tasks. If your product needs to show a user their own history, capture each result as it is delivered.