Guides

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-hub starter 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.

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.

The part between the --- lines is the frontmatter.

What each frontmatter key does here
KeyWhat it does here
scheduleA five-field cron: minute 7, hour 9, any day of the month, any month, Monday.
timeoutEach round may take up to 600 seconds.
max_roundsThe agent gets at most two fresh attempts to finish.
publish_artifactsGives 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

Terminal
hubzoid schedule run my-hub monday-brief --dry-run

This 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

Terminal
hubzoid schedule run my-hub monday-brief

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

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 wrote the brief in the task's own working folder before publishing it. Your wording will differ:

my-hub/.hubzoid/schedule/monday-brief@admin-localhost-7ba684f1/brief.md
# 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.md
How 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, the My Hub agent on its Workflows tab: Monday brief with the first sentence of its instructions, the status line On. Runs every Monday at 09:07 (server local time) with the next run, Runs as You (the local owner), and a View runs button.Settings, the My Hub agent on its Workflows tab: Monday brief with the first sentence of its instructions, the status line On. Runs every Monday at 09:07 (server local time) with the next run, Runs as You (the local owner), and a View runs button.
One line says whether the task will run and when. View runs opens its history.

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

Settings, Activity, Runs: one run of md:monday-brief on My Hub with the status Succeeded and a result that starts with Published.Settings, Activity, Runs: one run of md:monday-brief on My Hub with the status Succeeded and a result that starts with Published.
Markdown tasks and code workflows share one run history.

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.json in 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 9 rather than 0 9 keeps 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
FieldValues
Minute0 to 59
Hour0 to 23
Day of month1 to 31
Month1 to 12
Day of week0 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 run to 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.

my-hub/schedule/weekday-digest.md
---
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_artifact publishes 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_email sends plain text to that same account, using the deployment's SMTP settings (HUBZOID_SMTP_*). With HUBZOID_EMAIL_DELIVERY=preview it writes to a preview outbox instead. The tool reports accepted when the SMTP server took the message and previewed when 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:

my-hub/schedule/weekly-summary.md (frontmatter)
commit: ["output/summaries/"]
push: true
What 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 ends done, Hubzoid commits exactly those paths with the message schedule(<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 runs git pull --rebase and then git push, and retries the push once. A rebase conflict is aborted cleanly: the run is recorded as error, 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
my-hub/schedule/nightly-export.md
---
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

  1. Due, or started by hand
  2. Round: a fresh agent session
  3. STATUS: DONE or CONTINUE
  4. Commit and push, if declared
  5. Result in Settings
A run is a loop of rounds. Each round starts fresh and resumes from the state file.

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:

Finish 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 withWhat happens
STATUS: DONEThe run ends as done. The summary becomes the commit message when the task commits.
STATUS: CONTINUEThe next round starts with the note.
No status lineThe next round starts and is told to check the state file.
The round's timeoutThe round is cut off and the next one resumes from the state file.
A model or connection errorThe next round starts. Three failed rounds in a row end the run as error.
max_rounds reachedThe 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
ToolWhat 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.

PathHolds
.hubzoid/schedule/<task>@<person>/state.jsonThe task's progress for the account the run acts as, written by the agent.
.hubzoid/schedule/<task>@<person>/runs/<time>.jsonlOne 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.jsonPer 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

Terminal
hubzoid schedule run <hub> <task> [--timeout SECONDS] [--max-rounds N] [--model ID] [--dry-run]
Options for one run
OptionEffect for this run
--timeoutSeconds per round instead of the task's value.
--max-roundsRound limit instead of the task's value.
--modelAnother model. Ignored for run: tasks.
--dry-runPrint 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

ToDo this
Keep a task but stop its scheduleSet enabled: false. Settings then says "Off. Turn it on with enabled: true" in the file.
Stop a task without editing the filehubzoid 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 serverSet HUBZOID_DISABLE_SCHEDULE=1 and restart the Hub. Scheduled evals stop too.
Start a task nowhubzoid 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.

my-hub/schedule/monday-brief.md (frontmatter)
alert_to:
  - email: ops@example.com
  - slack: OPS_SLACK_WEBHOOK_URL   # an environment variable that holds the URL

A 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 seeWhat 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 1The file was rejected. The message names what to fix. Other tasks keep running.
The task never runs on a Hub that was already runningThe scheduler starts only when the Hub starts with a task. Restart hubzoid run my-hub.
A new task has not run yetIt 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 restartThe Hub stopped during the work. The next slot runs the task again, or start it now.
More problems and fixes
What you seeWhat to do
git pull --rebase failedThe remote moved and the rebase conflicted. Resolve it on the server. The commit is local and nothing was pushed.
is not inside a git repositoryThe task has commit: but the Hub is not in a git checkout. Use write: or set up the repository.
has no account to run asAdd run_as:, set HUBZOID_WORKFLOW_USER, or sign in once as the owner.
A restricted tool is refused in a runGrant the permission to the account the task runs as.

More cases are in operating runs.

Next steps

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.