Workflow API
Every part of the code workflow API, from the @workflow and @step decorators to the hub object, settings, schedule grammar and exceptions.
Every public part of the code workflow API. For a guided introduction, start with code workflows. For markdown tasks, see markdown tasks.
| Part | What it is |
|---|---|
@workflow | Declares a durable workflow: schedule, run account, webhook, inputs, chat tool, work items. |
@step | A durable step whose result is saved. |
hub | The run's context: inputs, settings, secrets, state, model and agent calls, reports, email. |
| Work items and people | hub.work_item, hub.operation, stations, handoffs, hub.approve, hub.ask. |
| Helpers for hub tools | current_user and data_dir for your own tools. |
workflows/settings.yaml | Engine limits, named webhooks, alerts, email lists, personal schedules. |
| Schedule grammar | The phrases and cron a schedule accepts. |
run_local | Run a workflow once in a test. |
| Exceptions, Runs, Environment variables | What can be raised, what a run records, what is read. |
Imports
from hubzoid import workflow, step, hubimport hubzoid does not import the workflow engine. workflow, step and hub are loaded on first access. current_user and data_dir are also exported for your own tools (see helpers for hub tools). Exceptions and the test helper are imported from their own modules:
from hubzoid.workflows.schedule_grammar import ScheduleError
from hubzoid.workflows.inputs import InputError
from hubzoid.workflows.identity import IdentityError
from hubzoid.structured import ModelOutputError
from hubzoid.runtime import AgentRunError
from hubzoid.artifacts import ArtifactError
from hubzoid.email_delivery import EmailError
from hubzoid.jev import JevError
from hubzoid.stations import StationOutputError
from hubzoid.work_items import Route
from hubzoid.workflows.testing import run_localHubzoid imports workflow modules itself. It loads every .py file in each folder directly under workflows/ and skips files whose names start with _. The engine imports them when it starts and when hubzoid schedule run runs a workflow. Hubzoid also imports them in a mode where the decorators only record each declaration, to build chat and MCP tools before the engine starts, and parses them without running them for Settings, hubzoid doctor and hubzoid schedule list. Module-level code can therefore run more than once, so keep it free of side effects. Importing a workflow file in plain Python raises RuntimeError, because the decorators need the engine.
Code workflows run where HUBZOID_SCHEDULES=1 is set for a hubzoid run of one Hub, and always in a deployment that hubzoid run starts from hubzoid.yaml or with several Hubs. That setting also turns on the Run for me tools and personal schedules. A Hub whose workflows/settings.yaml declares a webhook loads its code workflows for webhook runs even without it, but its schedules do not fire. hubzoid schedule run runs a workflow either way.
@workflow
workflow(schedule: str | None = None, *, timezone: str | None = None,
run_as: str | None = None, on_webhook: str | None = None,
catch_up: bool = False, timeout: float | str | None = None,
concurrency: int | None = None, concurrency_key: str | None = None,
alert_to: list | None = None, on_failure: str | None = None,
inputs=None, schedule_input: dict | None = None, max_input_kb: int | None = None,
as_tool: bool = False, confirm: bool = False, tool_wait: float | str | None = None,
surfaces: tuple | list | None = None, sensitive: bool = False,
schedulable: bool = False, min_interval: float | str | None = None,
work_items=None)Declares a durable workflow. The decorated function takes no parameters. Every parameter is optional.
Prop
Type
- The workflow name is the function's
__name__. A second workflow in the Hub whose name differs only in case raisesValueError. - Declarations are checked when the module loads. An unparseable schedule raises
ScheduleErrorand an unknown timezone raises the standard library'sZoneInfoNotFoundError. Arun_asthat is not an email, aconcurrencythat is not a positive integer, an undeclared webhook, a second consumer of the same webhook, a timeout that is not finite and positive, and a malformedalert_toraiseValueError. The inputs and chat options below have their own checks. - Always call the decorator. Use
@workflow()for a workflow without a schedule. - Write
schedule,timezone,run_as,on_webhook,as_tool,schedulable,sensitiveandsurfacesas literals. Settings,hubzoid doctorandhubzoid schedule listread them from the source without running it, and a value they cannot read keeps the Hub's code workflows from loading. - 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. Settings 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 and raises a
run_failedalert. See Alerts.
Inputs
Prop
Type
- Inputs are validated against the model on every run. Serialized as JSON they may be at most 4,000 characters unless
max_input_kbraises the limit. A field the model does not declare is refused unless the model allows extra fields. A run whose inputs no longer match fails withInputError, whose message is safe to show. - Keep inputs short and never secret. They are stored with the run, in a saved schedule and in the run details in Settings.
Field(json_schema_extra={"hidden": True})shows a value as •••• in lists. - A webhook workflow cannot have required inputs. Give them defaults.
hubzoid schedule run <hub> <name> --input key=valueparses values that look like JSON (numbers,true, lists) and keeps everything else as text.
Run for me and personal schedules
Prop
Type
as_tool=Trueadds a tool named after the function and described by the first line of its docstring. It needs the grant Run followed by the workflow's title (wf_<name>), which nobody holds until an administrator grants it in Settings. The function name may be at most 61 characters, and a workflow namedschedule_workflow,my_schedules,change_scheduleormy_reportsis not offered.- The tools exist only where code workflows run (see Imports), and
HUBZOID_WORKFLOW_TOOLS=falseremoves them. - The tool's run acts as the person asking, never the workflow's
run_as, and checks their grant again when it starts. It refuses anonymous callers, Slack and calls made inside a run. - Return
{"answer": "..."}to show text in chat after any report links, up to 8,000 characters and marked as data. Return{"refused": "..."}to show an expected refusal as it is, up to 500 characters. schedulable=Truelets people with Schedule reports for themselves (schedule_reports) save, change, pause and stop their own schedules in chat and under Schedules in Settings (/settings#/schedules). Saving or changing one takes a proposal and a confirmation. Each run acts as its owner. A schedule pauses with a reason after repeated failures, or when the owner's access or the inputs no longer match. The Hub's limits live underpersonal_schedulesinworkflows/settings.yaml, described below.- A personal run gets its own queue partition, so it never waits behind another person's runs or the team's.
- The chat tools that list, schedule and change these runs are described in Built-in tools.
Alerts
alert_to, on_failure and alerts.to in workflows/settings.yaml choose where a workflow's alerts go. The first that is set wins: alert_to, then on_failure, then alerts.to, then the deployment's HUBZOID_ALERT_URL.
| Destination | Form | Delivery |
|---|---|---|
| Webhook | {"webhook": "https://..."} or {"webhook": "ENV_VAR_NAME"} | A POST with the JSON payload below. |
| Slack | {"slack": "https://..."} or {"slack": "ENV_VAR_NAME"}, an incoming webhook | A short text message with the kind, the workflow and the Settings link. |
{"email": "ops@example.com"} | Sent with the HUBZOID_SMTP_* settings. With HUBZOID_EMAIL_DELIVERY=preview it is written to .hubzoid/alert-preview/. |
The webhook payload is JSON:
Prop
Type
- No run output or exception text is ever sent. It stays on the server.
- Webhook requests carry
Idempotency-KeyandX-Hubzoid-Timestamp. WithHUBZOID_ALERT_SECRETset they also carryX-Hubzoid-Signature,sha256=plus the HMAC-SHA256 of the timestamp, a.and the raw body. - Delivery is durable and tried up to five times, at least once. A receiver may see a duplicate after an ambiguous failure, so use the
Idempotency-Key. - Failed runs and timeouts have a cooldown, one hour by default. Occurrences inside it are counted in the next alert.
hubzoid schedule deliveries <hub>lists alert deliveries, including missing destinations and exhausted attempts.
@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)). Plain and async functions both work.
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.
- Before it runs, a step checks the run's deadline and that this process still owns the Hub's engine. A step that starts after the deadline raises
TimeoutError. - The result is stored in the run history, and Settings 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 the run's deadline and that the run's account is still usable, so a run whose account was blocked stops at its next call.
hub.event
hub.event is the verified webhook delivery that started the run, or None for every other trigger. A workflow that also runs on a schedule or from Settings must handle None.
Prop
Type
Use hub.event.key to make writes to other systems idempotent. A retried event may repeat work the first attempt did.
hub.call_llm
hub.call_llm(prompt: str, *, response_format: str = "text", response_model=None,
model: str | None = None, system: str | None = None,
timeout: float | str = 120)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 failure raises the call's own last error, such as
ModelOutputErrororTimeoutError. 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, timeout: float | str = 600,
agent: str | None = None, stage: str | None = None, revision: int = 1,
checkout: dict | None = 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 connector tools use that account's own connections, never the author's or an administrator's. - Returns only the agent's answer as
str, or an instance ofresponse_model. Tool activity, reasoning and download notes are left out. - 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",
timeout: float | str = 90) -> 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=(), key: str | None = None, digest: str | None = None) -> dictPublishes an existing file as an 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. With akeyit also returnslatest_urlandseries_id,share_urlwhen the report has a company link, andunchanged: truewhen nothing new was stored. - Without a
key, 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.
- A refused publish raises
ArtifactErrorwith an HTTP-stylestatusand amessagethat is safe to show.
Live reports
- Team or personal. A team run writes the agent's report,
/portal/latest/<agent>/<key>. Run for me and personal schedule runs write that person's own copy, at its own link, private to them whatever theaudienceuntil they share it from the viewer. - Unchanged copies store nothing. When the file, or
digest, matches the newest copy, the call returns that copy withunchanged: true. For a team report the sharing must also match, so a changed audience or reader list stores a new copy. Earlier copies keep their own access. - One workflow per key. Another workflow in the agent that uses the same key gets
ArtifactErrorwith status409. - Pages that stop. When a scheduled run no longer publishes a key it published before, that page says "No longer updating" and keeps its last copy.
- Company links.
audience="domain"needs akey, company sign-in set up for the deployment withHUBZOID_SHARE_DOMAINS, and the run's account holding Share reports with the company domain (share_domain_links). Anyone at those domains who has the link can read the report after signing in, without access to the agent. Otherwise the call raisesArtifactError. Company sign-in is covered in authentication. - Retention.
series_keepinworkflows/settings.yamlkeeps only the newest copies of each report. Without it, every copy is kept.
hub.send_email
hub.send_email(subject: str, body: str = "", *, artifacts=(), email_list: str | None = None,
raise_on_failure: bool = True, body_format: str = "text") -> dictEmails the run's own account. There is no recipient parameter, so neither code nor a model can address anyone else. The one exception is a list the builder declares in workflows/settings.yaml, from a team run.
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, or the account cannot receive mail or is blocked). - A body is at most 1,000,000 characters. Publish long content as a report and link it.
- 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. A local account such asadmin@localhostcannot receive mail, so use a real account or preview. See the configuration reference. - The call is a step. Failures before the message is transferred are tried up to three times. A resumed run never sends an accepted message again, and an
ambiguoussend is never resent automatically.
With email_list:
- Each address must belong to an account in the deployment or be at a domain in
HUBZOID_SHARE_DOMAINS. Other addresses are refused and blocked accounts are skipped. A list has at most 50 addresses. - The body is at most 2,000 characters and gets a footer naming the list. Links point to each report's company link or live link.
- The result has
status(accepted,previewed,partialorrefused),sent,resultswith one entry per address, andmessage. Apartialresult raisesEmailErrorunlessraise_on_failure=False. - From a Run for me or personal schedule run, the call is refused.
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.data_dir returns .hubzoid/data/ in the Hub, a private folder for the Hub's own data files that lasts across runs. Agent file tools cannot read it, and backups include it.
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. A personal run's state therefore belongs to that person. 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 workflows ran as people belongs to no account. Only a legacy run 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
Work items, stations and people
Work items
work_items= on @workflow makes each run a durable work item: one piece of work with a stable key, a route of stages it may run, and a state that survives restarts, redeliveries and upgrades. Finished stages are saved, so they never run or cost twice. Start one with hubzoid work-items start --workflow <name> --key <key>.
from hubzoid import hub, workflow
from hubzoid.work_items import Route
@workflow(work_items={
"answer": Route(stages=("plan",), actions=()),
"act": Route(stages=("plan", "check"), actions=("email.send",)),
})
def desk():
plan = hub.work_item.checkpoint("plan", lambda: draft_reply(hub.work_item.input))
if not hub.work_item.allows("check"):
return plan
hub.work_item.checkpoint("check", lambda: check_reply(plan))hub.work_item member | What it does |
|---|---|
id, generation, route, requester, input | The item's identity, its route and who asked for it. |
checkpoint(stage, run, *, response_model=None) | Runs one stage once and saves its output. A recovered or continued run gets the saved output. A stage outside the route is refused before it runs. |
allows(stage) | Whether the item's route includes the stage. |
narrow(route) | Moves the work to a narrower route of the same workflow. Widening is never allowed from inside a run. |
messages() | Follow-ups people sent since the last call, each {id, from, text, at}, returned once. |
reserve(counter, key, limit) | Counts something once per key, up to a limit. Returns the key's number, or None at the limit. |
Operations
hub.operation.run(...) performs one external write, such as sending an email or submitting an ERP document. It is recorded before anything is sent, and an uncertain send is never repeated on its own.
hub.operation.run(name: str, *, target: dict, arguments: dict, recovery: str,
dispatch, reconcile=None, actions=None, channel: str | None = None,
draft=None, target_version=None, provider_key_ttl: float | None = None,
key: str | None = None)| Argument | Effect |
|---|---|
dispatch(arguments) | Sends. Returns a JSON-serializable result. |
draft(arguments) | Saves a real draft instead, when the action's write mode is draft. |
recovery | What happens when the outcome is unknown: idempotent, provider_key (needs provider_key_ttl), reconcile (needs reconcile) or escalate to a person. |
actions | The action ids this write performs, such as email.send. Inside a work item it is required and must be in the route. A consequential action follows its write mode in work-items.yaml: off, draft or approve. |
channel, key | The channel it goes out on, and a key that tells deliberate repeats of the same name and target apart. |
Decide waiting and uncertain operations with hubzoid operations. See the SDK guide work items (opens in a new tab).
Stations
hub.call_agent(task, agent="analyst", stage="analysis") calls an isolated agent declared with isolated: true in agents/ (see Hub files). It gets its own instructions and exact tools only, its result is checked against its schema, and the call is saved so it never runs twice. A failed or malformed result raises StationOutputError (or StationError) with a call_id. hub.retry_station(call_id, clarification) makes the one retry. Stations pass documents with hub.save_handoff and hub.read_handoff. See the SDK guide stations (opens in a new tab).
People in the loop
These wait durably: the run holds no process while it waits and continues after a restart. The person asked is the account the run acts as, unless approvers names people by email or account id. When nobody else is named, that person is also emailed a link to answer.
| Call | Returns |
|---|---|
hub.approve(action, *, details=None, approvers=None) | {"approved": bool, "note": str | None, "by": email}. The person sees details exactly. |
hub.ask(question, *, options=None, approvers=None) | The answer text. options is 2 or 3 labels, and the person may type their own answer. |
hub.review_plan(path, *, title=None, approvers=None) | {"approved", "note", "by"} for one exact plan file, published as an artifact. Editing the plan makes a new version that needs its own approval. A request for changes is not an approval. |
Requests appear in the Inbox and in hubzoid requests.
Helpers for hub tools
Two helpers work in workflows and in your own Python tools in tools_local/ and restricted/:
| Helper | Returns |
|---|---|
hubzoid.current_user(hub_dir=None) | Inside a run, hub.user. In chat or MCP, the signed-in caller, with the same id, email, attrs and can(). None for an anonymous or system call. |
hubzoid.data_dir(hub_dir=None) | The Hub's private data folder, <hub>/.hubzoid/data/, created on first use. The same folder as hub.data_dir. |
workflows/settings.yaml
A YAML mapping at workflows/settings.yaml. It is checked when the bridge starts. A file that is not valid YAML, is not a mapping or has an invalid value below turns workflows off for that Hub, with the error in workflow health. Chat, and every other Hub run with it, keep running, and the Hub's webhooks accept no events until the file is fixed and the bridge restarts. Durations below are seconds, or strings such as "90s", "15m" or "1h".
Prop
Type
Named webhooks
webhooks:
ticket-updated:
verify: header
header: X-Webhook-Secret
event_key: "{ticket.id}:{ticket.modifiedTime}"Prop
Type
- The secret is the environment variable
WEBHOOK_SECRET_<NAME>, the webhook name in upper case with-written as_. Without it every delivery is refused. - With
hmac, the signature issha256=plus the hex HMAC-SHA256 of the timestamp, a.and the raw body. - Names are 1 to 64 lowercase letters, digits,
-or_, starting with a letter or digit.whatsapp,telegramand the Hub'sWEBHOOK_INBOUND_NAME(defaultwebhook) are refused, because the inbound receiver already serves them. - A sender POSTs a JSON object of at most 256 KiB to
/webhooks/<hub>/<name>. A200means the event is stored, not that the run succeeded. A repeat attaches to the stored event, and different content under the same key answers409and raises acontent_mismatchalert. Every response is listed in HTTP endpoints. - Hubzoid retries a failed event after each of
webhook_retry_delaysand then marks it failed with an alert.hubzoid schedule deliveries <hub>lists events andhubzoid schedule redrive <event-id> --hub <hub>retries a failed one. Cancelling a webhook run fails its event, for an explicit redrive.
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. - Times follow the local clock of the timezone. On the day clocks go back, a repeated time fires once. On the day they go forward, a time that does not exist fires at the first moment after the gap.
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 not backfilled, except that a workflow with catch_up=True runs its latest missed slot once after downtime. Schedules fire only where HUBZOID_SCHEDULES=1 is set or several Hubs run together, and never for a paused workflow.
Test with run_local
from pathlib import Path
from hubzoid.workflows.runtime import load_definitions
from hubzoid.workflows.testing import run_local
hub_dir = Path("my-hub").resolve()
wf = next(w for w in load_definitions(hub_dir) if w.name == "weekly_report")
out = run_local(wf.fn, hub_dir=hub_dir, inputs={"period": "week"}, as_user="sam@example.com")
out.result, out.artifacts, out.emailsrun_local(fn, *, hub_dir, inputs: dict | None = None, as_user: str | None = None,
trigger: str = "tool", event: dict | None = None) -> LocalRunRuns a workflow's function once, in this process, without the engine. Pass the function from load_definitions, because importing a workflow file directly raises RuntimeError.
Prop
Type
- Returns a
LocalRunwithresult, the validatedinputs,artifacts(one dict per publish withpath,title,key,audience,digestandscope) andemails(one dict per send with its subject, body, artifacts and list). - Nothing is published or sent. Publishes return
local://links and emails returnpreviewed. - Steps run directly, without checkpoints.
hub.stateandhub.data_diruse the Hub's real store and folder. hub.call_llm,hub.call_agentandhub.call_jevraiseRuntimeErrorunless the test wires them withhubzoid.workflows.context.configure.
Exceptions
Prop
Type
Runs
Prop
Type
A markdown task run returns a result with this shape, shown as the run's result in Settings 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, opts in to publishing and email with publish_artifacts: true and send_email: true, and chooses its alert destinations with alert_to:. 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).

