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.

PartWhat it is
@workflowDeclares a durable workflow: schedule, run account, webhook, inputs, chat tool, work items.
@stepA durable step whose result is saved.
hubThe run's context: inputs, settings, secrets, state, model and agent calls, reports, email.
Work items and peoplehub.work_item, hub.operation, stations, handoffs, hub.approve, hub.ask.
Helpers for hub toolscurrent_user and data_dir for your own tools.
workflows/settings.yamlEngine limits, named webhooks, alerts, email lists, personal schedules.
Schedule grammarThe phrases and cron a schedule accepts.
run_localRun a workflow once in a test.
Exceptions, Runs, Environment variablesWhat can be raised, what a run records, what is read.

Imports

workflows/weekly_report/main.py
from hubzoid import workflow, step, hub

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

Imports
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_local

Hubzoid 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

Signature
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 raises ValueError.
  • Declarations are checked when the module loads. An unparseable schedule raises ScheduleError and an unknown timezone raises the standard library's ZoneInfoNotFoundError. A run_as that is not an email, a concurrency that 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 malformed alert_to raise ValueError. 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, sensitive and surfaces as literals. Settings, hubzoid doctor and hubzoid schedule list read 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 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. 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_failed alert. 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_kb raises 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 with InputError, 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=value parses values that look like JSON (numbers, true, lists) and keeps everything else as text.

Run for me and personal schedules

Prop

Type

  • as_tool=True adds 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 named schedule_workflow, my_schedules, change_schedule or my_reports is not offered.
  • The tools exist only where code workflows run (see Imports), and HUBZOID_WORKFLOW_TOOLS=false removes 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=True lets 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 under personal_schedules in workflows/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.

DestinationFormDelivery
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 webhookA short text message with the kind, the workflow and the Settings link.
Email{"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-Key and X-Hubzoid-Timestamp. With HUBZOID_ALERT_SECRET set they also carry X-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

Signature
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 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 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

Signature
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 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 failure raises the call's own last error, such as ModelOutputError or TimeoutError. 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

Signature
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 workflow surface. 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 of response_model. Tool activity, reasoning and download notes are left out.
  • 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

Signature
hub.call_jev(state, questions: dict, *, model: str = "typesafe/jev-1.13",
             timeout: float | str = 90) -> 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

Signature
hub.publish_artifact(path, *, title: str | None = None, audience: str = "owner",
                     share_with=(), key: str | None = None, digest: str | None = None) -> dict

Publishes 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, size and audience. url is the viewer, https://<host>/portal/artifacts/<id>, which asks the person to sign in. With a key it also returns latest_url and series_id, share_url when the report has a company link, and unchanged: true when 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 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.
  • A refused publish raises ArtifactError with an HTTP-style status and a message that 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 the audience until they share it from the viewer.
  • Unchanged copies store nothing. When the file, or digest, matches the newest copy, the call returns that copy with unchanged: 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 ArtifactError with status 409.
  • 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 a key, company sign-in set up for the deployment with HUBZOID_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 raises ArtifactError. Company sign-in is covered in authentication.
  • Retention. series_keep in workflows/settings.yaml keeps only the newest copies of each report. Without it, every copy is kept.

hub.send_email

Signature
hub.send_email(subject: str, body: str = "", *, artifacts=(), email_list: str | None = None,
               raise_on_failure: bool = True, body_format: str = "text") -> dict

Emails 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, 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, 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_FROM and the other HUBZOID_SMTP_* settings, or writes to .hubzoid/outbox/<person>/ with HUBZOID_EMAIL_DELIVERY=preview. A local account such as admin@localhost cannot 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 ambiguous send 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, partial or refused), sent, results with one entry per address, and message. A partial result raises EmailError unless raise_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>.

workflows/desk/main.py
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 memberWhat it does
id, generation, route, requester, inputThe 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.

Signature
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)
ArgumentEffect
dispatch(arguments)Sends. Returns a JSON-serializable result.
draft(arguments)Saves a real draft instead, when the action's write mode is draft.
recoveryWhat happens when the outcome is unknown: idempotent, provider_key (needs provider_key_ttl), reconcile (needs reconcile) or escalate to a person.
actionsThe 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, keyThe 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.

CallReturns
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/:

HelperReturns
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

workflows/settings.yaml
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 is sha256= 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, telegram and the Hub's WEBHOOK_INBOUND_NAME (default webhook) are refused, because the inbound receiver already serves them.
  • A sender POSTs a JSON object of at most 256 KiB to /webhooks/<hub>/<name>. A 200 means the event is stored, not that the run succeeded. A repeat attaches to the stored event, and different content under the same key answers 409 and raises a content_mismatch alert. Every response is listed in HTTP endpoints.
  • Hubzoid retries a failed event after each of webhook_retry_delays and then marks it failed with an alert. hubzoid schedule deliveries <hub> lists events and hubzoid 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.

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.
  • 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

tests/test_weekly_report.py
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.emails
Signature
run_local(fn, *, hub_dir, inputs: dict | None = None, as_user: str | None = None,
          trigger: str = "tool", event: dict | None = None) -> LocalRun

Runs 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 LocalRun with result, the validated inputs, artifacts (one dict per publish with path, title, key, audience, digest and scope) and emails (one dict per send with its subject, body, artifacts and list).
  • Nothing is published or sent. Publishes return local:// links and emails return previewed.
  • Steps run directly, without checkpoints. hub.state and hub.data_dir use the Hub's real store and folder.
  • hub.call_llm, hub.call_agent and hub.call_jev raise RuntimeError unless the test wires them with hubzoid.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).

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.