Hubzoid
Reference

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, hub

import 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 JevError

Hubzoid 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 raises ValueError.
  • The schedule and timezone are validated when the module loads. An unparseable schedule raises ScheduleError. An unknown timezone raises the standard library's ZoneInfoNotFoundError.
  • Always call the decorator. Use @workflow() for a manual workflow.
  • Write schedule, timezone and run_as as string literals. The Console, hubzoid doctor and hubzoid schedule list read 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 with IdentityError and never falls back to another account. See Workflows and schedules.
  • Each run binds hub to the run, with that account on the workflow surface and the settings from workflows/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_failure set, 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 status on the server shows every result.
  • Read secrets with hub.secret inside 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 withReturns
Neither response_format="json" nor response_modelstr, the reply text
response_format="json"dict. A reply that is valid JSON but not an object raises ModelOutputError
response_model=ModelAn instance of Model, validated with Pydantic
  • For JSON, Hubzoid appends an instruction to reply with only a JSON object, with the schema when response_model is given. LiteLLM models also get the provider's JSON mode. claude-local and codex-local run 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 workflow surface. 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 of response_model.
  • A failed agent run raises AgentRunError. JSON that is missing or does not validate raises ModelOutputError.
  • The call is a step. It is tried once unless agent_max_attempts in workflows/settings.yaml is 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") -> dict

Experimental. 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 noul answer holds the probability of yes, a choice answer one of your labels with probabilities, and a score answer 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 to OPENROUTER_API_KEY or 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=()) -> dict

Publishes 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, size and audience. url is 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 is HUBZOID_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) -> dict

Emails 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, recipient and message. status is accepted (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) or refused (not tried, for example because email is not configured).
  • It sends over SMTP with HUBZOID_SMTP_HOST, HUBZOID_SMTP_FROM and the other HUBZOID_SMTP_* settings, or writes to .hubzoid/outbox/<person>/ with HUBZOID_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 ambiguous send 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.

FormExamplesCron
Minute interval: every N with minute, minutes, min or mevery 2 minutes, every 15 min, every 5m*/N * * * *
Hour interval: every N with hour, hours, hr or hevery 1 hour, every 3 hours, every 6h0 */N * * *
Daily: daily or every day, optional at, then a timedaily at 6am, daily 06:30, every day at 18:00MM HH * * *
Weekly: every, a day, optional at, then a timeevery monday 08:30, every fri at 5pmMM HH * * D
Raw cron: five fields of digits and * / , -30 6 * * 1-5Unchanged, 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:MM or HH:MM on a 24-hour clock, or an hour with an optional am or pm, such as 6, 6am, 6 pm or 18. 12am is midnight and 12pm is noon. Minutes are written in the 24-hour form only.
  • A day is sunday or sun, monday or mon, tuesday or tue, wednesday or wed, thursday or thu, friday or fri, saturday or sat. 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 ScheduleError when 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).

Next steps

Read this page as Markdown

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.