Workflows and schedules
How a Hubzoid agent does work on its own, on a schedule, from a webhook or when someone asks in chat, and whose account each run uses.
A Hub can do work without anyone chatting: on a clock, when a webhook arrives, when an operator starts it, or when someone asks for a report or a repeating request in chat. All of it runs on one durable engine inside the Hub process, with the same knowledge, tools and access rules as chat.
When to use it
There are three ways to set up recurring work. Start with the simplest one that fits.
| Repeated request | Markdown task | Code workflow | |
|---|---|---|---|
| Who sets it up | Anyone who uses the agent, in chat | A builder, as a file | A builder, in Python |
| File | None. Saved in Hubzoid. | schedule/<task>.md | workflows/<name>/*.py |
| You write | One request, such as "every Monday at 9, list the items below their reorder level" | Instructions for the agent, or a run: command | Exact steps with @workflow and @step, calling the model where judgment helps |
| Runs as | The person who asked | The task's run_as:, else the Hub default | The workflow's run_as=, else the Hub default. A report run from chat acts as the person asking. |
| Guide | Schedules and reports | Markdown tasks | Code workflows |
Write a markdown task when you can describe the job in a paragraph. Move to a code workflow when the steps must be exact, when a threshold or loop decides what happens, when you call your own APIs, or when people should run it for themselves from chat. hubzoid new workflow <name> <hub> scaffolds one.
A small example
A markdown task is one file. This one asks the Hub's agent for a short brief every Monday:
---
schedule: '7 9 * * 1' # Mondays at 09:07, server time
timeout: 600
max_rounds: 2
publish_artifacts: true
---
Write a short Monday brief for the support team from knowledge/house-rules.md:
support hours, the refund window and the large-order rule, each in one line.
Publish it as a report titled "Monday brief". You are done when it is published.Run it once now instead of waiting for Monday:
hubzoid schedule run my-hub monday-brief✓ done in 1 round(s): Published "Monday brief" (support hours, 30-day refund window, $500
large-order approval) from knowledge/house-rules.md.The agent's Workflows tab in Settings shows the task, when it runs next and whose account it runs as:


How it works
- Trigger fires
- Run queued
- Steps checkpointed
- Result recorded
- Shown in Settings
One engine
Every run executes on the Hub's embedded DBOS (opens in a new tab) engine. There is no separate job server.
Where run history lives, which runs start where, and engine locks
The engine keeps its run history in .hubzoid/dbos.db, or in PostgreSQL when HUBZOID_DBOS_DB or a PostgreSQL DATABASE_URL is set. Workflow state, pauses, personal schedules and the access audit live in the operational database, so every bridge on a shared database sees the same pauses.
Markdown tasks and both kinds share one run history, the same hubzoid schedule pause, resume and cancel commands, and the same hold while hubzoid backup copies the deployment.
- Markdown tasks and scheduled evals start whenever their files exist.
HUBZOID_DISABLE_SCHEDULE=1turns them off. - Code workflows, reports people run from chat and personal schedules run only where the deployment sets
HUBZOID_SCHEDULES=1, or automatically in a deployment thathubzoid runstarts fromhubzoid.yamlor with several Hubs. A copy of the Hub on a laptop does not fire alongside the server.
One engine owns a Hub at a time, with a file lock on SQLite and an advisory lock on PostgreSQL. A second bridge for the same Hub keeps chat running and takes over when the owner releases the Hub.
Runs and steps
A run is one execution of a task or workflow, with an id, a status, a result and a list of steps. A step is a unit of work whose result the engine saves before the run moves on. When a process restarts, a run resumes after its last completed step.
The steps of each kind of run, and run ids
Every run first records the account it acts as, in a step named hz_md_identity or hz_run_identity, so a resumed run never switches account midway. A markdown run then has up to four steps: the agent's work (or the run: command), a commit of the paths the task declares under commit:, a push when it sets push: true, and finish. A code workflow's steps are your @step functions and each call to hub.call_llm, hub.call_agent, hub.call_jev, hub.publish_artifact or hub.send_email. Code outside a step runs again when a run resumes, so keep side effects inside steps.
A scheduled run's id is derived from its slot, such as md:monday-brief:20261026T0907@my-hub, so the same slot is never queued twice.
Triggers
A markdown task has exactly one trigger: schedule: or on_webhook:. Code workflows run on a schedule phrase, from a named webhook, by hand or from chat.
Every trigger and how it works
| Trigger | Applies to | How it works |
|---|---|---|
| Cron | Markdown tasks | schedule: takes a numeric 5-field cron in the local time of the machine running the Hub. Checked every 30 seconds. |
| Schedule phrase | Code workflows | @workflow("daily at 6am", timezone="Europe/London") takes a phrase or a raw cron in the workflow's time zone, UTC by default. Checked once a minute. |
| Generic webhook | Markdown tasks | on_webhook: <name> runs the task while verified events from the generic webhook surface wait in its inbox. |
| Named webhook | Code workflows | @workflow(on_webhook="<name>") runs once per event of a webhook declared in workflows/settings.yaml, served at /webhooks/<hub>/<name>. See webhooks. |
| Manual | Both | hubzoid schedule run <hub> <name>, with --input key=value for a workflow's inputs, or Run now in Settings for a code workflow. A workflow declared with @workflow() and no schedule runs only on request. |
| From chat | Code workflows | A report offered in chat runs for the person asking, or on a personal schedule they saved. |
Repeated requests
Anyone who uses an agent can ask in chat for something to happen on a schedule:
Repeat this for me every Monday at 9: list the stock items below their reorder level.The agent shows the exact request, when it runs and where answers go, and saves it only after a yes. Each run asks the agent that request as the person, with their current access, and puts the answer in a new private chat.
Time zones, managing requests and turning them off
Times use the person's time zone from Settings unless they name one, then the deployment's, then the zone their browser sends, then UTC.
People run, pause, resume, edit and stop their requests under Settings → My schedules. HUBZOID_PROMPT_WORKFLOWS=false turns the feature off for a Hub. On a laptop where schedules are off, the agent says it cannot schedule.
Reports people run and schedule
A code workflow can be a report people run from chat for themselves and keep fresh on their own schedule. Every option is off until a workflow opts in and an administrator grants the matching capability.
A workflow opts in on its decorator:
# SalesReport is a Pydantic model of the inputs: region and period.
@workflow("daily at 6am", timezone="Europe/London", run_as="reports@example.com",
inputs=SalesReport, schedule_input={"region": "north"},
as_tool=True, schedulable=True)
def sales_report():
"""Sales for one region, for the week or the month so far."""
...Inputs, Run for me, access, personal schedules and live reports
- Inputs.
inputs=takes a Pydantic model, read in the run ashub.input. Chat and MCP tools take its schema, Run now in Settings shows a form, and the CLI takes--input key=value. Inputs are stored with the run, so never put a secret in them. - Run for me.
as_tool=Trueadds a chat and MCP tool that runs the workflow as the person asking, with their grants.schedulable=Truelets people save their own schedule of it. - Access. Each report adds a capability to the agent's Access tab, such as Run Sales report (
wf_sales_report). Schedule reports for themselves lets a person save schedules, and Share reports with the company domain lets a run publish a company link. Workflow permissions on the same tab gives finer rights per workflow, such as run, schedule, see runs or pause. - Personal schedules. "Run the sales report for me every Monday at 9" saves a schedule after a yes. Each run acts as that person, so it stops when their access does. A schedule pauses itself after three failures in a row, or when the person's access or the workflow's inputs change. By default a person may keep 10 schedules per agent, at most hourly.
- Live reports.
hub.publish_artifact(..., key=...)adds each publish to a report series with one link that always shows the newest copy. A team run writes the agent's series. Run for me and personal schedules write that person's own series, private to them until they share it.
Administrators see everyone's schedules under Team schedules, with their model cost for the last 30 days, and can pause or stop them. The Hub must be running when a schedule is due.
Workflows that wait for a person
A code workflow can stop and ask. hub.ask(question, options=...) waits for an answer, hub.approve(action) waits for a yes or no, and hub.review_plan(path) waits for someone to approve a plan. The run holds no process while it waits and continues after a restart. The question appears as a card in the chat that started the run and in the person's Inbox. See code workflows.
Who a run acts as
Every run acts as an ordinary Hubzoid account: a person, or an account your team creates for shared automation, such as reports@example.com. There is no separate service account type. For scheduled, webhook, Settings and CLI runs, the first match wins:
run_ason the declaration:@workflow(..., run_as="priya@example.com"), orrun_as:in a markdown task's frontmatter.HUBZOID_WORKFLOW_USERin the Hub's configuration: a Hub secret, else<hub>/.env.HUBZOID_WORKFLOW_USERin the deployment's configuration: the deployment secret or the.envnext tohubzoid.yaml.- The owner recorded at setup: the configured initial owner (
HUBZOID_ADMIN_EMAIL), recorded the first time it signs in. A local Hub with sign-in off usesadmin@localhost.
A Run for me run, a personal schedule and a repeated request act as the person who asked. A team run started from chat with run_workflow acts as the workflow's own account and records who started it.
What run_as checks, what belongs to the account and older service identities
run_as chooses whose permissions a run uses and grants nothing. It is read only from files and operator configuration, never from an API, a tool or a model. The account must have signed in at least once, not be awaiting approval, blocked or replaced, and hold Use this agent. Otherwise the run fails with an error that names the fix, and Hubzoid never falls back to another account. The account is checked again before every model call, tool call, publish, email and connection.
What the run touches belongs to that account:
| Belongs to | |
|---|---|
| Restricted tools | The account's own grants, checked at every call. |
hub.state | The run's account. The same workflow run for someone else starts empty. |
hub.shared_state | Everyone who runs the workflow. Keep personal data out of it. |
hub.run_dir | This run only, a private folder under .hubzoid/runs/ that agent file tools cannot read. |
| A markdown task's state folder | The run's account, .hubzoid/schedule/<task>@<person>/. |
| Artifacts and email | The run's account. hub.send_email reaches that account, or an email list declared in workflows/settings.yaml from a team run. |
| Personal connections | The account's own connections to connectors added in Settings, used through hub.call_agent. Never the author's or an administrator's. |
Older releases ran a code workflow as the service identity workflow:<name> and a markdown task as workflow:md:<task>. Those grants are kept and labelled Legacy service identity in Settings, but no run uses them. When the account lacks a permission the old identity held, the run log names it. See identity and access.
Running safely
These rules decide how many runs happen at once, what can repeat after a restart, what happens to missed slots and when a run raises an alert.
Concurrency
Markdown tasks run one at a time per Hub, and code workflows run one at a time per workflow by default.
Queues, concurrency settings and caps
- Markdown tasks run one at a time per Hub, and scheduled evals share that queue. A due task waits while the Hub is answering chat, then starts on a later tick. A running task continues alongside new chat requests.
- Code workflows run one at a time per workflow by default, and different workflows run side by side.
@workflow(concurrency=N)allows more, andconcurrency_keykeeps runs that share a key, such as one ticket, in order.max_concurrent_workflows: Ninworkflows/settings.yamlcaps the shared queue. There is no cap by default. - Markdown tasks and code workflows use separate queues, so one of each can run at the same time.
Delivery semantics
Code workflows run at least once, so make every step that changes something outside the Hub safe to repeat. Markdown tasks never repeat their work.
What repeats after a restart, and how to make steps safe to repeat
Code workflows run at least once. A completed step is not run again when a run resumes. A step that was running when the process stopped runs again, so its side effects can happen twice. hub.publish_artifact and hub.send_email are recorded steps: a resumed run gets back what it already published and never sends an accepted email again. A hub.call_jev call still in flight when the process stopped is made again, so it can be billed twice. Make every step that changes something outside the Hub safe to repeat:
- Use a key the other system deduplicates on, such as a record id, the run's slot or the webhook event's key.
- Check before you write: does the ticket for this alert already exist?
- Record what you finished as a per-item marker, for example
hub.state[f"done:{item_id}"] = True, and skip marked items.
Markdown tasks never repeat their work. If the process stops mid-work, the recovered run fails with an error that starts interrupted by a restart, and the task's state file carries progress to the next run. A run that finished its work but stopped before commit or push resumes from that step.
Retries, timeouts and webhook redelivery are in the workflow API and operating runs.
Missed runs and catch-up
A markdown task runs once to catch up. A code workflow is not backfilled, and with catch_up=True only the latest missed slot runs once.
Every missed-run situation for tasks and workflows
| Situation | Markdown task | Code workflow |
|---|---|---|
| The Hub was down across one or more slots | Runs once on the first tick after start | Not backfilled, and Settings warns about the gap. With catch_up=True, the latest missed slot runs once, unless the next slot is close. |
| A new file appears | First runs at its next future match | Loads when the Hub next starts, then runs at its next slot |
| Paused, then resumed | Runs once if a slot passed while paused | Runs at its next slot |
| A backup held new runs | Runs once when the hold ends | The latest slot inside the hold runs when it ends. Earlier ones are counted as missed. |
| The dispatcher fell behind by several slots | Covered by the single catch-up run | Only the latest slot runs. Earlier ones are counted as missed. |
On a machine where code workflow schedules are off, no catch-up runs and Settings says the schedules are off instead of showing a next run.
Alerts and deadlines
Alerts are off until you choose where they go: a webhook, a Slack incoming webhook or an email address, per Hub under alerts.to in workflows/settings.yaml, per workflow with alert_to=, per markdown task with alert_to:, or HUBZOID_ALERT_URL for the whole deployment.
What raises an alert, what it carries, and deadlines
Then failed runs, failed scheduled evals, overdue runs, failure streaks and a schedule that stopped firing raise alerts, with a one-hour cooldown. An alert carries the Hub, the kind, the workflow, the run id and a link to Settings, never outputs or exception text. Failed runs always show in Settings → Activity, alerts or not.
A schedule whose scheduled runs fail 20 times in a row pauses itself (pause_after_failures, 0 turns it off). Webhook runs have a 15-minute deadline by default, and scheduled runs have none until you set @workflow(timeout=...) or workflow_timeout. Python code that is already running cannot be stopped at its deadline: an overdue run raises an alert and keeps running until it returns.
When workflow code changes
Runs belong to the code that started them. Markdown task edits apply without a restart, and a code workflow change needs one.
How code versions are tracked and when each file is read
Runs belong to the code that started them: a hash of the installed Hubzoid version and every workflows/**/*.py file. Code a workflow imports from outside workflows/ is not part of the hash. When the Hub starts under new code, runs that were queued or interrupted under the old code are cancelled and stay in the list as CANCELLED. A markdown run that was queued but had not started is queued again under the new code.
- The scheduler re-reads
schedule/*.mdon every tick, so markdown task edits apply without a restart. workflows/settings.yamlis read when the bridge starts and at the start of each code workflow run. A mistake in it turns that Hub's workflows off, with the error in workflow health, while chat keeps running.- Code workflow modules load when the Hub starts. Restart the Hub to pick up a code change, and drain queued runs first as described in operating runs.
How runs appear in Settings
- An agent's Workflows tab lists its code workflows and markdown tasks with their schedule, state and Runs as. View runs opens a workflow's runs, and a run shows how it started, its timing, its steps and the account it ran as.
- Run now starts a code workflow, with a form for its inputs, for organization administrators and people with Run and control workflows.
- Activity → Runs lists runs across agents. Pauses, resumes, cancels and runs started from chat are recorded in Activity → Audit log.
- My schedules lists each person's repeated requests, then their reports. Team schedules shows everyone's, for administrators.
A run's result belongs to the account it acted as. See runs in Settings.
Who sees a run's result and details
The Hub's managers see each run's workflow, status, timing and a failure summary. The result, step outputs and detailed errors are shown only to that account. hubzoid schedule status on the server shows everything.

