Every team has one: the colleague who sends the Monday report before anyone asks. Nobody messages them on Monday morning. They just know it is due, pull the numbers, and post it where people will see it.
What makes that colleague safe to rely on is not only that they remember. It is that they work inside limits. They have a company card with a ceiling on it. If the reporting system is down, they try a couple of times and then tell their manager, instead of retrying every five minutes all week. And their manager can see what they sent, and can tell them to stop.
An AI employee on Wetel can do the same kind of unprompted work through a scheduled workflow. This post is for the operations lead deciding what to hand over and the developer setting it up: what a schedule is, the three controls that stop it running away, and the one gap you must cover yourself today.
What is a schedule?
A schedule runs one AI employee’s published workflow on a timer, with no message from anyone. “Every weekday at 9am”, “every 15 minutes”, “the first of every month”: at each of those times the platform starts a run of that agent’s workflow, the same way an on-demand task run (runWorkflowTask) does.
A few facts set the boundaries:
- A person creates and owns it. Every schedule operation needs a dashboard login (a JWT), not an API key, and every schedule is scoped to your workspace. A schedule id from another workspace behaves as if it does not exist.
- It points at an agent, not a workflow. At every fire it runs whatever workflow is currently published on that agent. Republish the agent’s workflow and the next fire runs the new version.
- The timing is a standard cron expression (five fields: minute, hour, day of month, month, day of week) in a timezone you choose. The default timezone is UTC.
- A dashboard Schedules page is being rolled out. Where you don’t see it yet, the GraphQL mutations do the same job.
What does a scheduled run look like?
It is a run with nobody on the other end, so it behaves differently from a chat. Plan the workflow around three properties.
There is no user message. The run starts with an empty message and no conversation history. Anything the workflow needs comes from the schedule’s own variables (a flat map of simple values you set once) or from steps that fetch it, such as an action or tool node calling your own systems.
Nobody is waiting for a reply. A response node’s text lands in the session’s transcript, but no chat window is listening. If the run should deliver something (post a digest, update a record, notify someone), the workflow has to send it outward itself, usually with a final webhook node or an action node.
Every fire is asynchronous. Creating or updating a schedule tells you nothing about whether any run succeeded. You read outcomes afterwards (more on that below).
flowchart LR C["Clock reaches the cron time"] --> Q{"Spend cap reached?<br/>Agent active and published?"} Q -- no --> F["Fire counts as failed<br/>(no run starts)"] Q -- yes --> R["Run the agent's published workflow<br/>(no user message)"] R --> A["action / webhook node<br/>sends the result outward"] R --> S["Schedule row updated:<br/>lastRunAt, lastRunStatus"] F --> SAn example: the weekday ops digest
This is the example from the reference, unchanged: a digest every weekday at 09:00 Kuala Lumpur time.
mutation CreateSchedule($input: CreateScheduledWorkflowInput!) { createScheduledWorkflow(input: $input) { id name status cronExpression timezone nextRunAt externalConversationIdTemplate maxConsecutiveFailures }}{ "input": { "name": "Weekday morning ops digest", "agentId": 12, "cronExpression": "0 9 * * 1-5", "timezone": "Asia/Kuala_Lumpur", "variables": { "reportType": "daily_ops", "lookbackHours": 24 }, "externalConversationIdTemplate": "digest:{{date}}", "maxConsecutiveFailures": 3 }}Reading it line by line:
cronExpression: "0 9 * * 1-5"means 09:00, Monday to Friday.timezonemakes “09:00” mean 09:00 in Kuala Lumpur (nextRunAtcomes back in UTC, so 01:00).variablesare the facts every run starts with. Here the workflow learns which report to build and how far back to look, without anyone typing it.externalConversationIdTemplate: "digest:{{date}}"decides which session each fire uses. With{{date}}, fires on the same local day share a session and the next day starts a new one. The default (scheduled:{{scheduleId}}:{{firedAt}}) gives every fire a fresh session with no history, which is the safe choice unless you want runs to see earlier runs.maxConsecutiveFailures: 3is the “stop after three failed attempts” rule from the opening. More on it next.
The cron expression is checked strictly when you save, so a mistake fails at once rather than at 9am. It must have exactly five fields: the six-field form with seconds is refused, so the shortest interval is one minute. Shortcuts like @daily are refused too; write 0 0 * * * instead.
The three controls that stop it running away
A schedule has no person in the loop at the moment it fires, which makes it the easiest way to spend money by accident. Three controls apply to every schedule.
1. Plan quota: how many schedules you can hold
Your plan tier sets how many schedules your workspace may have. On the documented tiers that is 1 on DEV and 25 on PRO. Paused and auto-disabled schedules still count; only deleting a schedule frees its slot. Going over the limit is refused with a message that names your limit.
2. Monthly spend cap: checked before every fire
Every fire checks your workspace’s monthly language-model spend cap before it starts anything (the documented caps are $5 on DEV and $200 on PRO). If the cap is reached, the fire does not run, and it counts as a failed run for the next control. That is the company card with a ceiling.
3. Consecutive-failure auto-disable: stop after repeated failures
When a schedule fails maxConsecutiveFailures times in a row, it switches itself off: its status becomes AUTO_DISABLED, its next run time is cleared and it stops firing. The default is 3, and you can set anything from 1 to 20. Any successful run resets the count to zero, so one bad morning does not trip it; only a run of failures does.
What counts as a failure:
- a run that ends
FAILED - a fire that cannot start at all: the spend cap is reached, the agent is not
ACTIVE, the agent has no published workflow, or the session id the template renders is invalid
Runs that end COMPLETED, AWAITING_INPUT or INTERRUPTED count as successes.
The gap you must cover: an auto-disabled schedule tells nobody
When a schedule switches itself off, no notification is sent today: no email and no webhook. It simply stops. The only trace is on the schedule itself: status: AUTO_DISABLED, the consecutiveFailureCount, and lastRunStatus: "failed". The dashboard’s Schedules page highlights it, but only while someone has that page open.
So the colleague who stopped after three failed attempts does stop, but does not yet walk over to tell the manager. Until that alerting exists, check for it yourself. Two ways:
Poll the schedule list. This query is cheap, and you can run it on a timer from your own tooling and raise your own alert when any schedule reads AUTO_DISABLED:
query ScheduleHealth { scheduledWorkflows(first: 100) { totalCount edges { node { id name status lastRunAt lastRunStatus consecutiveFailureCount nextRunAt } } pageInfo { endCursor hasNextPage } }}Read the per-agent roll-up. agentActivitySummary includes schedule health (a schedulesAutoDisabled count), and its needsAttention flag turns true for an agent with a schedule that was switched off after repeated failures.
When you find one, fix the cause first, then call resumeScheduledWorkflow. Resuming resets the failure count and starts the timer again. If you resume without fixing anything, it will fail the same number of times and switch itself off again. Look at lastRunStatus, the failing session’s runs, and whether the workspace has hit its spend cap.
How to see what a scheduled run actually did
The schedule row gives you the headline:
lastRunAtis stamped when a fire starts.lastRunStatusreads"running"while it runs, then"completed","failed","awaiting_input"or"interrupted". It is a lowercase string, andnullmeans the schedule has never fired.consecutiveFailureCountshows how close it is to switching itself off.
For the step-by-step detail, find the fire’s session and read its runs. With the default template, every session for schedule 7 has an id starting scheduled:7:, so you can find them through sessions(agentId:) and then ask for the runs:
query ScheduleRuns($sessionId: Int!) { workflowRuns(sessionId: $sessionId) { id status errorMessage nodeTrace context }}nodeTrace lists every step the run visited, in order, which is how you check “the report went out” against evidence rather than assumption. One catch: a fire that never started (spend cap reached, agent inactive, nothing published) has no run to read. For those, the schedule’s own lastRunStatus and failure count are the only record.
Before you switch one on
A short checklist, all from the reference:
- Publish the agent’s workflow first. Creating a schedule only checks that the agent exists in your workspace, not that it is active or has a published workflow. A schedule pointing at an unpublished agent is accepted, then fails every fire until the breaker trips.
- Make sure the workflow sends its result somewhere. A webhook or action node at the end, not only a
responsenode. - Gate any id before an outbound write. If a variable feeds a write to another system, put a
conditionnode in front and testsomeId > 0, neversomeId != 0. - Pick a failure limit you can live with. Three is the default. Lower means it stops sooner; higher means more failed attempts, each of which may cost money, before it does.
- Decide who checks on it, and how often, since nothing will tell them.
What is live today, and what is not built yet
Live today:
- Schedules on a five-field cron with a timezone, created and managed by a signed-in person in your workspace.
- The three controls: plan quota, the monthly spend cap checked before every fire, and auto-disable after repeated failures.
- Run records you can read: the schedule’s own status fields, each fire’s
workflowRuns, and schedule health inagentActivitySummary.
Not built yet:
- Event-based triggers. A workflow can start because a clock fired, not yet because something happened in another system (“when a record changes, run this”). That is on the roadmap as direction, not a dated promise.
- Alerting when a schedule switches itself off. No email, no webhook and no push notification today. Poll for it, as above.
A scheduled AI employee gets the colleague’s limits today. What is still missing is the colleague telling their manager when they had to stop, so for now a person on your team checks.
Related reference docs
- Scheduled workflows: every schedule operation, field and error
- Cost controls: plan quota, spend cap and auto-disable: what counts as a failure and how resuming works
runWorkflowTask: the on-demand, API-key version of the same headless runagentActivitySummary: per-agent roll-up including schedule healthwebhooknode: the usual way a scheduled run delivers its result- Observability and debugging: reading
nodeTracefor any run - Pricing & Plans: the schedule quota and spend cap per tier
- Roadmap: event-based triggers and supervision alerts, both not built yet
To set up your first one, start with the scheduled workflows reference.