Concepts

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 requestMarkdown taskCode workflow
Who sets it upAnyone who uses the agent, in chatA builder, as a fileA builder, in Python
FileNone. Saved in Hubzoid.schedule/<task>.mdworkflows/<name>/*.py
You writeOne request, such as "every Monday at 9, list the items below their reorder level"Instructions for the agent, or a run: commandExact steps with @workflow and @step, calling the model where judgment helps
Runs asThe person who askedThe task's run_as:, else the Hub defaultThe workflow's run_as=, else the Hub default. A report run from chat acts as the person asking.
GuideSchedules and reportsMarkdown tasksCode 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:

my-hub/schedule/monday-brief.md
---
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:

Terminal
hubzoid schedule run my-hub monday-brief
Output
✓ 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:

Settings, the My Hub agent on its Workflows tab: the Monday brief task, On, runs every Monday at 09:07 server local time, runs as You (the local owner), with a View runs button.Settings, the My Hub agent on its Workflows tab: the Monday brief task, On, runs every Monday at 09:07 server local time, runs as You (the local owner), with a View runs button.
Every task and workflow shows its schedule, its state and the account it runs as.

How it works

  1. Trigger fires
  2. Run queued
  3. Steps checkpointed
  4. Result recorded
  5. Shown in Settings
Every scheduled, webhook, chat and manual run follows the same path through the Hub's engine.

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=1 turns 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 that hubzoid run starts from hubzoid.yaml or 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
TriggerApplies toHow it works
CronMarkdown tasksschedule: takes a numeric 5-field cron in the local time of the machine running the Hub. Checked every 30 seconds.
Schedule phraseCode 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 webhookMarkdown taskson_webhook: <name> runs the task while verified events from the generic webhook surface wait in its inbox.
Named webhookCode workflows@workflow(on_webhook="<name>") runs once per event of a webhook declared in workflows/settings.yaml, served at /webhooks/<hub>/<name>. See webhooks.
ManualBothhubzoid 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 chatCode workflowsA 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:

Message
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:

workflows/sales_report/main.py
# 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 as hub.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=True adds a chat and MCP tool that runs the workflow as the person asking, with their grants. schedulable=True lets 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:

  1. run_as on the declaration: @workflow(..., run_as="priya@example.com"), or run_as: in a markdown task's frontmatter.
  2. HUBZOID_WORKFLOW_USER in the Hub's configuration: a Hub secret, else <hub>/.env.
  3. HUBZOID_WORKFLOW_USER in the deployment's configuration: the deployment secret or the .env next to hubzoid.yaml.
  4. 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 uses admin@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 toolsThe account's own grants, checked at every call.
hub.stateThe run's account. The same workflow run for someone else starts empty.
hub.shared_stateEveryone who runs the workflow. Keep personal data out of it.
hub.run_dirThis run only, a private folder under .hubzoid/runs/ that agent file tools cannot read.
A markdown task's state folderThe run's account, .hubzoid/schedule/<task>@<person>/.
Artifacts and emailThe run's account. hub.send_email reaches that account, or an email list declared in workflows/settings.yaml from a team run.
Personal connectionsThe 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, and concurrency_key keeps runs that share a key, such as one ticket, in order. max_concurrent_workflows: N in workflows/settings.yaml caps 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
SituationMarkdown taskCode workflow
The Hub was down across one or more slotsRuns once on the first tick after startNot 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 appearsFirst runs at its next future matchLoads when the Hub next starts, then runs at its next slot
Paused, then resumedRuns once if a slot passed while pausedRuns at its next slot
A backup held new runsRuns once when the hold endsThe latest slot inside the hold runs when it ends. Earlier ones are counted as missed.
The dispatcher fell behind by several slotsCovered by the single catch-up runOnly 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/*.md on every tick, so markdown task edits apply without a restart.
  • workflows/settings.yaml is 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.

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.