Workflow API
Reference for Hubzoid code workflows. The @workflow and @step decorators, the hub object, the account a run acts as, workflow state, artifacts, email, the schedule grammar, settings and exceptions.
This page lists every public part of the code workflow API in Hubzoid 1.0.1. For a guided introduction, start with Code workflows. For markdown tasks, see Markdown tasks.
Imports
from hubzoid import workflow, step, hubimport hubzoid does not import the workflow engine. workflow, step and hub are loaded on first access. Exceptions are imported from their own modules:
from hubzoid.workflows.schedule_grammar import ScheduleError
from hubzoid.structured import ModelOutputError
from hubzoid.runtime import AgentRunError
from hubzoid.workflows.identity import IdentityError
from hubzoid.email_delivery import EmailError
from hubzoid.jev import JevErrorHubzoid imports workflow modules itself, after it starts the hub's engine: when the hub starts, and when hubzoid schedule run runs a workflow. It loads every .py file in each folder directly under workflows/ and skips files whose names start with _. The decorators need the engine, so importing a workflow file in plain Python raises RuntimeError.
@workflow
workflow(schedule: str | None = None, *, timezone: str | None = None, on_failure: str | None = None,
run_as: str | None = None)Declares a durable workflow. The decorated function takes no parameters.
Prop
Type
- The workflow name is the function's
__name__. A second workflow with the same name in the hub raisesValueError. - The schedule and timezone are validated when the module loads. An unparseable schedule raises
ScheduleError. An unknown timezone raises the standard library'sZoneInfoNotFoundError. - Always call the decorator. Use
@workflow()for a manual workflow. - Write
schedule,timezoneandrun_asas string literals. The Console,hubzoid doctorandhubzoid schedule listread them from the source without running it. - Each run first resolves the account it acts as and records it as the step
hz_run_identity. A resumed run reuses it. Without a usable account the run fails withIdentityErrorand never falls back to another account. See Workflows and schedules. - Each run binds
hubto the run, with that account on theworkflowsurface and the settings fromworkflows/settings.yaml. - The return value becomes the run's result. Return plain data such as a dict. The Console shows the first 8,000 characters of a result or error to the account the run acted as. The hub's managers see a failure summary instead.
- An exception that leaves the function marks the run failed. With
on_failureset, Hubzoid sends the notification first and then re-raises, so the failure is never hidden.
The on_failure POST has a 10 second timeout and this JSON body:
Prop
Type
@step
step(fn=None, *, max_attempts: int = 1)Marks a function as a durable step. Use it bare (@step), called (@step()) or with options (@step(max_attempts=3)).
Prop
Type
- A completed step's result is saved. When a run resumes after a restart, the saved result is returned and the function is not called again.
- A step that was running when the process stopped runs again, so delivery is at least once. Make side effects idempotent.
- The result is stored in the run history, and the Console shows the first 4,000 characters of each step's output or error to the account the run acted as. Do not return secrets, because
hubzoid schedule statuson the server shows every result. - Read secrets with
hub.secretinside the step that uses them rather than passing them in.
hub
hub is a proxy bound to the current run, so concurrent runs never see each other's context. Outside a running workflow, its members raise RuntimeError, except secret, which only reads the environment.
Prop
Type
Every model, agent, Jev, publish and email call first checks that the run's account is still usable, so a run whose account was blocked stops at its next call.
hub.call_llm
hub.call_llm(prompt: str, *, response_format: str = "text", response_model=None,
model: str | None = None, system: str | None = None)Prop
Type
| Called with | Returns |
|---|---|
Neither response_format="json" nor response_model | str, the reply text |
response_format="json" | dict. A reply that is valid JSON but not an object raises ModelOutputError |
response_model=Model | An instance of Model, validated with Pydantic |
- For JSON, Hubzoid appends an instruction to reply with only a JSON object, with the schema when
response_modelis given. LiteLLM models also get the provider's JSON mode.claude-localandcodex-localrun one turn with no tools. - The reply is parsed from the whole text, then a fenced code block, then the outermost braces or brackets. A reply with no valid JSON, or one that fails validation, raises
ModelOutputError. - The call is a step that is tried twice in total, because it has no side effects. A completed call is not repeated when a run resumes.
- Each call writes a usage row with the run's account, the model, tokens, estimated cost and duration.
hub.call_agent
hub.call_agent(task: str, *, response_model=None)Prop
Type
- Runs the hub's full agent, with its tools, skills, knowledge and model, as the run's account on the
workflowsurface. Restricted tools need that account's grant, and personal MCP tools come from that account's own Open WebUI connections, never the author's or an administrator's. - Returns the agent's reply as
str, or an instance ofresponse_model. - A failed agent run raises
AgentRunError. JSON that is missing or does not validate raisesModelOutputError. - The call is a step. It is tried once unless
agent_max_attemptsinworkflows/settings.yamlis above 1, because a retry can repeat a message or a write the first attempt made. - Each call writes a usage row.
hub.call_jev
hub.call_jev(state, questions: dict, *, model: str = "typesafe/jev-1.13") -> dictExperimental. Asks TypeSafe's Jev, a decision model, through OpenRouter's Decisions API, which is in alpha, so its shapes may change.
Prop
Type
- Returns a dict of answers by question name. A
noulanswer holds the probability of yes, achoiceanswer one of your labels with probabilities, and ascoreanswer a value within the scale with probabilities. Every answer is checked against its question. - Needs
JEV_OPENROUTER_API_KEY, a dedicated OpenRouter key. It never falls back toOPENROUTER_API_KEYor the hub model. - Failures raise
JevError: a missing or rejected key, missing credits, invalid questions, or an empty or malformed reply. Rate limits, server errors and timeouts are retried once. - The call is a step. A completed call is not repeated on resume, but one still in flight when the process stopped is made again, so a call is at least once. Each call writes one usage row.
hub.publish_artifact
hub.publish_artifact(path, *, title: str | None = None, audience: str = "owner", share_with=()) -> dictPublishes an existing file as a new artifact owned by the run's account. The owner cannot be chosen by the caller.
Prop
Type
- Returns
id,url,title,filename,content_type,sizeandaudience.urlis the viewer,https://<host>/portal/artifacts/<id>, which asks the person to sign in. - Every call stores a new artifact, so earlier ones are never overwritten. Files are kept under
.hubzoid/artifacts/, which agent file tools cannot read. The largest file isHUBZOID_ARTIFACT_MAX_BYTES, 50 MiB by default. - A workflow can never create a public link. The owner creates one in the viewer, and only with the Share artifacts publicly permission.
- The call is a step. A resumed run gets back the artifact it already published. It needs the run to act as a person, not a legacy service identity.
hub.send_email
hub.send_email(subject: str, body: str = "", *, artifacts=(), raise_on_failure: bool = True) -> dictEmails the run's own account. There is no recipient parameter, so neither code nor a model can address anyone else.
Prop
Type
- Returns
status,sent,delivery_id,recipientandmessage.statusisaccepted(the SMTP server accepted the message, which does not confirm it reached the inbox),previewed(written to the outbox, not sent),failed,ambiguous(the connection ended while the server was receiving it) orrefused(not tried, for example because email is not configured). - It sends over SMTP with
HUBZOID_SMTP_HOST,HUBZOID_SMTP_FROMand the otherHUBZOID_SMTP_*settings, or writes to.hubzoid/outbox/<person>/withHUBZOID_EMAIL_DELIVERY=preview. See the configuration reference. - The call is a step. Failures before the message is transferred are retried. A resumed run never sends an accepted message again, and an
ambiguoussend is never resent automatically.
hub.run_dir
hub.run_dir returns a Path to a folder for this run only, .hubzoid/runs/<workflow>/<run id>/ in the hub, created on first use. Agent file tools cannot read it. Write generated files there, then publish them.
hub.state
hub.state returns a WorkflowState for the current hub, workflow and the account the run acts as. Values are stored as JSON in the table hz_workflow_kv of the deployment's operational database, keyed by hub, workflow, account and key, so two workflows never clash on a key and the same workflow run for someone else starts empty. State survives restarts and upgrades. hub.shared_state returns the same kind of object for state shared by every account that runs the workflow. Keep personal data out of it.
State written before 1.0.1 is kept but belongs to no account. Only a legacy hub with no account configured still reads it.
Prop
Type
Each write is its own database commit and is not part of a step's checkpoint. A read, modify and write sequence is therefore not atomic across a crash and replay, and a counter can double count. Store per-item markers, such as hub.state[f"done:{item_id}"] = sha, and skip items that are already marked.
hub.user
hub.user returns a HubUser for the account the run acts as.
Prop
Type
workflows/settings.yaml
A YAML mapping at workflows/settings.yaml. A file that is not valid YAML or not a mapping fails each run that reads it.
Prop
Type
Schedule grammar
Code workflow schedules are case-insensitive phrases or raw cron expressions. Hubzoid translates them to a 5-field cron and evaluates it in the workflow's timezone.
| Form | Examples | Cron |
|---|---|---|
Minute interval: every N with minute, minutes, min or m | every 2 minutes, every 15 min, every 5m | */N * * * * |
Hour interval: every N with hour, hours, hr or h | every 1 hour, every 3 hours, every 6h | 0 */N * * * |
Daily: daily or every day, optional at, then a time | daily at 6am, daily 06:30, every day at 18:00 | MM HH * * * |
Weekly: every, a day, optional at, then a time | every monday 08:30, every fri at 5pm | MM HH * * D |
Raw cron: five fields of digits and * / , - | 30 6 * * 1-5 | Unchanged, after validation |
- A minute interval must divide 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20 or 30). An hour interval must divide 24 (1, 2, 3, 4, 6, 8 or 12).
- A time is
H:MMorHH:MMon a 24-hour clock, or an hour with an optionalamorpm, such as6,6am,6 pmor18.12amis midnight and12pmis noon. Minutes are written in the 24-hour form only. - A day is
sundayorsun,mondayormon,tuesdayortue,wednesdayorwed,thursdayorthu,fridayorfri,saturdayorsat. One day per phrase. - A raw cron uses numbers only. For several weekdays or other patterns, use a raw cron such as
0 9 * * 1-5. - Anything else raises
ScheduleErrorwhen the module loads.
The dispatcher wakes at the start of each minute and starts every workflow with a slot in the window since its last check. When several slots of one workflow fall in that window, only the latest runs and the others are counted as missed. Missed slots are never backfilled. Schedules fire only where HUBZOID_SCHEDULES=1 is set or under hubzoid gateway.
Exceptions
Prop
Type
Runs
Prop
Type
A markdown task run returns a result with this shape, shown as the run's result in the Console to the account the run acted as:
Prop
Type
A markdown task acts as an account in the same way, from run_as: in its frontmatter, and opts in to publishing and email with publish_artifacts: true and send_email: true. See Markdown tasks.
Environment variables
Prop
Type
See the configuration reference for every variable and the CLI reference for hubzoid schedule and hubzoid new workflow. The source is in hubzoid/workflows/ (opens in a new tab).
