Markdown tasks
Write recurring work as one Markdown file in schedule/, run it once to check it, then let your Hub run it on its own timetable.
A markdown task is one file in your Hub's schedule/ folder. The top of the file says when it runs. The rest is plain instructions that the Hub's own agent carries out on its own, with the same instructions, knowledge, tools and model it uses in chat.
In this guide you add a Monday brief to the my-hub starter, run it once, see it in Settings and leave it to run every Monday.
When to use a markdown task
Use one for recurring work you can describe in a paragraph: a weekly summary, a nightly check, keeping knowledge up to date. Use a code workflow when the steps must be exact, and a webhook when another system should start the work.
Markdown tasks run on their own whenever their file exists and the Hub is running. Code workflows also need HUBZOID_SCHEDULES=1, so a copy of a Hub on a laptop does not fire them by accident.
What you need
- The
my-hubstarter from the quickstart, with a model that answers in chat. - A terminal in the folder that holds
my-hub/.
Create the task file
Save this file as my-hub/schedule/monday-brief.md. The file name, without .md, is the task's name.
---
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.The part between the --- lines is the frontmatter.
What each frontmatter key does here
| Key | What it does here |
|---|---|
schedule | A five-field cron: minute 7, hour 9, any day of the month, any month, Monday. |
timeout | Each round may take up to 600 seconds. |
max_rounds | The agent gets at most two fresh attempts to finish. |
publish_artifacts | Gives the agent a tool to publish a file as a private report. |
The last sentence of the body says when the work is done. Always include one.
Check it without calling the model
hubzoid schedule run my-hub monday-brief --dry-runThis prints the exact prompt the agent receives in round 1 and exits. You see the rules Hubzoid adds (work unattended, keep progress in a state file, write only where allowed), then your instructions. Nothing is queued and no model is called.
hubzoid doctor my-hub also checks every task file, under schedule.tasks.
Run it once
hubzoid schedule run my-hub monday-briefThe command queues the task, waits for it and prints the result. The agent reads the house rules, writes the brief, publishes it and reports that it is done:
✓ 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 wrote the brief in the task's own working folder before publishing it. Your wording will differ:
# Monday brief
- **Support hours:** 09:00–18:00 UTC, Monday to Friday.
- **Refund window:** Unused items can be returned within 30 days of delivery for a full refund; after that, store credit or repair only.
- **Large orders:** Orders over $500 need manager approval before shipping.
Source: knowledge/house-rules.mdHow the working folder is named, and what else it holds
The folder name is the task's name, then @ and the account the run acted as. On your laptop that is the local owner, admin@localhost. The same folder holds state.json, the task's memory between runs, and a log of every run under runs/.
See it in Settings
Start the Hub if it is not running (hubzoid run my-hub). Open Settings → Agents → My Hub → Workflows.


Settings → Activity → Runs lists the runs of every agent you manage. The run you started appears as md:monday-brief:


Leave it to run
Nothing else is needed. While hubzoid run my-hub is running, the Hub runs the task every Monday at 09:07 in the server's local time.
The scheduler starts only when the Hub starts with at least one task, so restart hubzoid run after you add the first one. After that, it reads schedule/ again every 30 seconds, and edits apply without a restart.
Write instructions that work unattended
There is nobody to answer questions during a run, so write the body as a complete brief:
- Say what done means. "You are done when the report is written and the state file is updated."
- Use the state file. The agent keeps progress in
state.jsonin its working folder. It is the only memory between rounds and between runs, so write the body around it: "Your state file records the last date you reported." - Name every file. Say which files to read and which to write.
- Pick an off-minute.
7 9rather than0 9keeps your task apart from the many jobs that start on the hour.
Frontmatter
Every task needs exactly one trigger, schedule: or on_webhook:. An agent task also needs a body. Keys not listed here are ignored.
Every frontmatter key
Prop
Type
A rejected file is skipped and every other task keeps running. hubzoid schedule list prints the problem and exits 1.
Why a file is rejected, and how tasks are named
A file is rejected when a trigger is missing or doubled, the cron has the wrong number of fields or never matches a real date, a number is not a positive integer, a path is absolute or contains .., push: true has no commit: paths, model: is empty, run_as: is not an email, publish_artifacts: or send_email: is not true or false or is set on a run: task, or an agent task has no body. hubzoid doctor fails schedule.tasks, and the agent's Workflows tab shows the problem after "Can't run".
The task's name comes from the file name: letters, digits, ., _ and -, starting with a letter or digit. Settings shows it in words, so monday-brief.md reads Monday brief, with the first sentence of the body as its description. Files named README.md or starting with . are not tasks.
Cron and time
Tasks run in the local time of the machine that runs the Hub. Values are numbers only, so write 1 for Monday, not MON.
Cron fields and scheduling rules
| Field | Values |
|---|---|
| Minute | 0 to 59 |
| Hour | 0 to 23 |
| Day of month | 1 to 31 |
| Month | 1 to 12 |
| Day of week | 0 to 7, where 0 and 7 are Sunday |
Each field accepts *, */N, a value, a range A-B, a stepped range A-B/N or a comma list of these. When both day of month and day of week are restricted, a day matches if either one matches, as in standard cron.
- In a container, the local time is the container's local time.
- A new task first runs at its next future match. Use
hubzoid schedule runto try it at once. - After downtime, a task that missed one or more matches runs once, not once per missed match. The number of missed slots goes into its
missed_log, kept for 31 days. - A due task waits while the Hub is answering chat and starts on a later check.
- Markdown tasks run one at a time per Hub.
Publish and email results
A task opts in to delivery in its frontmatter. The model cannot turn these tools on, and cannot change the owner or the recipient.
---
schedule: "7 7 * * 1-5"
run_as: priya@example.com
publish_artifacts: true
send_email: true
---
Write today's digest as an HTML file in your working folder, publish it,
and email me a link.What each delivery tool does, and what code workflows add
- Reports.
publish_artifactpublishes a file the run wrote as a private report owned by the account the run acts as. That person can share it from the report's viewer. - Email.
send_emailsends plain text to that same account, using the deployment's SMTP settings (HUBZOID_SMTP_*). WithHUBZOID_EMAIL_DELIVERY=previewit writes to a preview outbox instead. The tool reportsacceptedwhen the SMTP server took the message andpreviewedwhen it only wrote to the outbox. - Nothing extra by default. Tasks without these keys are offered neither tool.
Live report links that always show the newest copy, company links, email lists and Markdown email are options of code workflows.
Write, commit and push
To keep the results in git, list the paths the task may change:
commit: ["output/summaries/"]
push: trueWhat write, commit and push do
write:paths can be changed but are never committed. Use them while testing and review the diff by hand.commit:paths are writable too. After a run that endsdone, Hubzoid commits exactly those paths with the messageschedule(<task>): <summary>. Other changes in the working tree are never swept in.- A run that changed none of its
commit:paths makes no commit and pushes nothing. - With
push: true, Hubzoid runsgit pull --rebaseand thengit push, and retries the push once. A rebase conflict is aborted cleanly: the run is recorded aserror, the commit stays local and nothing is pushed.
The agent itself cannot commit or push. Set up the server once: the repository needs a remote and a tracking branch, git config user.name and user.email, and credentials that work without a prompt, such as a deploy key.
Run a command instead of the agent
A task with run: runs a command in the Hub folder and does not call the model. It shares the same schedule, queue, log, state and commit steps.
An example command task and how it ends
---
schedule: "41 1 * * *"
run: ["python", "scripts/export_orders.py", "--days", "1"]
timeout: 600
commit: ["output/exports/"]
---
Writes yesterday's order export. Notes only.A string such as run: "python scripts/export.py | gzip > output/exports/latest.gz" runs through the shell. A list runs without one. Exit code 0 is done and any other code is error. At the timeout, the whole process group is stopped. The log keeps the last 4,000 characters of the command's output. The script finds the account the run acts as in HUBZOID_RUN_AS, for information only.
How a run works
- Due, or started by hand
- Round: a fresh agent session
- STATUS: DONE or CONTINUE
- Commit and push, if declared
- Result in Settings
Each round is a fresh agent session that receives Hubzoid's rules, your body and a note from the previous round. Fresh rounds let a large backlog be worked through in parts. The agent must end every round with one of these lines:
STATUS: DONE — <one-line summary of what changed>
STATUS: CONTINUE — <what remains>How each round ends, what happens next, and how Settings shows it
An incomplete run shows as succeeded, with incomplete in its result. Only error marks a run as failed in Settings.
Hubzoid matches that line, ignoring case, and the last one in the reply counts. A colon or hyphen after DONE also works.
| A round ends with | What happens |
|---|---|
STATUS: DONE | The run ends as done. The summary becomes the commit message when the task commits. |
STATUS: CONTINUE | The next round starts with the note. |
| No status line | The next round starts and is told to check the state file. |
The round's timeout | The round is cut off and the next one resumes from the state file. |
| A model or connection error | The next round starts. Three failed rounds in a row end the run as error. |
max_rounds reached | The run ends as incomplete. The next scheduled run resumes from the state file. |
hubzoid schedule run exits 1 for an incomplete run.
A run interrupted by a restart is not repeated. It is reported as interrupted, and the task runs again at its next slot. A run that finished its work but stopped before its commit or push resumes from there.
Tools during a run
The agent has the Hub's usual tools, two that exist only in scheduled runs, and the two delivery tools when the task opts in.
The tools only scheduled runs have
| Tool | What it allows |
|---|---|
run_git(repo, args) | Read and sync git in a checkout inside the Hub: pull, fetch, log, diff, show, status, rev-parse, ls-files, branch, shortlog, describe and read-only remote. Other commands are refused. |
write_hub_file(path, content) | Create or overwrite a file under the task's write: and commit: paths or its own working folder. Anything else is refused, including schedule/ and any .git/ folder. |
publish_artifact(path, title) | With publish_artifacts: true. Publish a writable file as a private report for the run's account and return its link. |
send_email(subject, body, artifact_ids) | With send_email: true. Email the run's account, optionally with links to reports published in the run. At most 5 per run. |
State and log files
Each task keeps its state and run logs under .hubzoid/schedule/, per account. Keep .hubzoid/ out of git.
Every state and log file, and state from older releases
The same task run for two people never shares state.
| Path | Holds |
|---|---|
.hubzoid/schedule/<task>@<person>/state.json | The task's progress for the account the run acts as, written by the agent. |
.hubzoid/schedule/<task>@<person>/runs/<time>.jsonl | One line per event: rounds, each tool call that writes or publishes, the agent's reply, the status and any error. tail -f follows a live run. |
.hubzoid/schedule-state.json | Per task: when it was first seen, when it last ran, the last result, the last log and its missed_log. |
Releases before runs acted as people kept state in .hubzoid/schedule/<task>/. That folder is left as it was and belongs to no one. To carry its state over, copy state.json into the person's folder before their first run.
Run once with hubzoid schedule run
hubzoid schedule run <hub> <task> [--timeout SECONDS] [--max-rounds N] [--model ID] [--dry-run]Options for one run
| Option | Effect for this run |
|---|---|
--timeout | Seconds per round instead of the task's value. |
--max-rounds | Round limit instead of the task's value. |
--model | Another model. Ignored for run: tasks. |
--dry-run | Print the round 1 prompt, or the command of a run: task, and exit. |
A manual run ignores the cron, pauses and the wait for chat. Exit codes: 0 done, 1 incomplete or failed, 2 no such task. See the CLI reference.
Queueing, run ids and webhook tasks
It joins the same one-at-a-time queue as scheduled runs, so it never overlaps one, and it appears in the run history with an id such as md:monday-brief:manual-20261025T210247. A manual run of a webhook task handles no events.
Pause, disable or stop
| To | Do this |
|---|---|
| Keep a task but stop its schedule | Set enabled: false. Settings then says "Off. Turn it on with enabled: true" in the file. |
| Stop a task without editing the file | hubzoid schedule pause my-hub monday-brief, recorded in the audit log. resume runs it once if a slot passed. |
| Stop every markdown task on a server | Set HUBZOID_DISABLE_SCHEDULE=1 and restart the Hub. Scheduled evals stop too. |
| Start a task now | hubzoid schedule run my-hub monday-brief, which works for disabled and paused tasks too. |
People granted Run and control workflows can also start, pause, resume and cancel a task from chat. See operating runs.
Alerts
When the Hub has alert destinations, a failed run and a streak of failures (3 by default) raise an alert, with a one-hour cooldown. See alerts.
Where destinations are set, an example alert_to and what an alert carries
A run that ends incomplete raises nothing. The Hub's destinations are under alerts.to in workflows/settings.yaml, and alert_to: in a task replaces them for that task.
alert_to:
- email: ops@example.com
- slack: OPS_SLACK_WEBHOOK_URL # an environment variable that holds the URLA webhook or slack entry names an environment variable that holds the address, which keeps it out of the Hub's files. An alert carries the Hub, the kind, the task, the run id, a count and a link to Settings, never the run's output. A task whose scheduled runs fail 20 times in a row is paused, with an alert.
Who the task runs as
A run acts as an ordinary account: run_as:, else HUBZOID_WORKFLOW_USER, else the owner recorded at setup. On a laptop that is you, the local owner, and the Workflows tab shows You (the local owner) under Runs as. See who a run acts as.
A missing account, restricted tools and model usage
If the account is missing or cannot be used, the run fails with the fix in its error and never falls back to someone else.
The task reaches a restricted tool only when that account holds its permission. Grant it in Settings on the agent's Access tab, or with hubzoid grant priya@example.com <permission> my-hub. Grants that older releases made to workflow:md:<task> are kept but no run uses them. Each round's model usage is counted for the run's account in Settings → Usage. See restrict tools.
If something goes wrong
| What you see | What to do |
|---|---|
no task or workflow 'monday-brief' under ... | Check the file is my-hub/schedule/monday-brief.md and that you passed the name without .md. |
hubzoid schedule list prints a problem and exits 1 | The file was rejected. The message names what to fix. Other tasks keep running. |
| The task never runs on a Hub that was already running | The scheduler starts only when the Hub starts with a task. Restart hubzoid run my-hub. |
| A new task has not run yet | It first runs at its next future match. Run it now with hubzoid schedule run. |
✗ incomplete after 2 round(s) | The agent did not end with STATUS: DONE. Raise timeout or max_rounds, or give the body a clear done condition and have it save progress in the state file. |
A run failed with interrupted by a restart | The Hub stopped during the work. The next slot runs the task again, or start it now. |
More problems and fixes
| What you see | What to do |
|---|---|
git pull --rebase failed | The remote moved and the rebase conflicted. Resolve it on the server. The commit is local and nothing was pushed. |
is not inside a git repository | The task has commit: but the Hub is not in a git checkout. Use write: or set up the repository. |
has no account to run as | Add run_as:, set HUBZOID_WORKFLOW_USER, or sign in once as the owner. |
| A restricted tool is refused in a run | Grant the permission to the account the task runs as. |
More cases are in operating runs.

