Hub files
Reference for every file and folder a Hub can contain, the frontmatter keys each loader reads, and the runtime folders Hubzoid creates.
A Hub is one folder. Hubzoid reads the files below from it every time the Hub starts, and some of them on every call. Everything except the .env files and the runtime folders belongs in Git.
Layout
| Path | Required | Read by | Purpose |
|---|---|---|---|
AGENTS.md | yes | every surface | The main agent's instructions and settings. |
.env | no | every command that builds or serves the Hub | Model, keys and settings. See configuration. |
agents/ | no | agent builder | Sub-agents, loaded as skills or run as delegates. |
skills/ | no | list_skills, load_skill | Playbooks the agent loads on demand. |
knowledge/ | no | list_knowledge, read_knowledge | Reference documents, read live from disk. |
tools_local/ | no | tool registry | Python tools. |
restricted/ | no | tool registry, access guard | Python tools that need a permission, plus their secrets in restricted/.env. |
connectors/.mcp.json | no | MCP loader | External MCP servers the agent can call. |
schedule/ | no | scheduler | Markdown tasks run on a cron or by a webhook. |
workflows/ | no | workflow engine | Python code workflows and their settings. |
evals/ | no | hubzoid eval, scheduler | Behaviour checks. |
identity/ | no | access, inbound surfaces | Roster of people and their groups, and permission labels. |
raw_data/ | no | grep_data, read_file, list_files | Large source material to search rather than load. |
branding/ | no | hubzoid run, hubzoid gateway | Logo, favicon, splash and CSS for the web chat. |
Folder name variants
Folder names match without regard to case, and these variants are accepted. When two variants exist in one Hub, the first alphabetically is used and a warning is logged.
| Canonical | Also accepted |
|---|---|
agents | agent |
skills | skill |
tools_local | tool_local, tools, local_tools |
connectors | connector |
schedule | schedules, scheduled |
workflows | workflow |
evals | eval, evaluations |
raw_data | raw-data, rawdata |
output | outputs |
knowledge, restricted and identity have no variants.
AGENTS.md
The main agent. The body is the system prompt, used verbatim. Frontmatter is optional, so a plain Markdown file works.
---
name: ops-desk
description: Answers stock and order questions for the operations team.
model: claude-local/sonnet
suggestions:
- Which items are below reorder level?
- Summarize yesterday's orders
mcp_instructions: |
Use read_knowledge for policy questions and stock_level for live counts.
---
You help the operations team of a home goods retailer with four stock locations.
Answer from knowledge files and tools. Never guess a stock count.| Key | Type | Default | Effect |
|---|---|---|---|
name | string | the Hub folder name | The agent's name. Its slug is the model id in /v1/models unless MODEL_LABEL is set, and it is the web chat's display name unless WEBUI_NAME is set. |
description | string | the first non-heading line of the body, up to 200 characters | Short summary. A gateway shows it in the model picker. |
model | model id | none | The Hub's model when .env does not set MODEL. MODEL in .env wins. |
suggestions | list of strings | none | Click-to-send prompts on the web chat's empty new-chat screen. |
mcp_instructions | string | the body | Instructions sent to MCP clients when they connect, in place of the body. Use it when the body holds internal guidance or is long. |
auto_addendum | boolean | true | false stops Hubzoid from appending its runtime section (knowledge index, skills index, tool guidance) to the instructions. |
A file with frontmatter but no body fails to load. tools: on the main agent has no effect: the main agent always has the full tool registry.
Agent definitions: agents/
Each sub-agent is either a folder, agents/<name>/AGENTS.md (or the first *.md in the folder), or a single file, agents/<name>.md. Hidden entries are skipped.
---
name: researcher
description: When the user wants a researched brief on a supplier or product.
model: claude-local/opus
tools: [web_search, http_get, read_knowledge]
---
Research the question with web_search and http_get, then write a short brief with sources.| Key | Type | Default | Effect |
|---|---|---|---|
name | string | the folder or file name | Identifier, and the skill name when loaded as a skill. |
description | string | the first non-heading line of the body | When the main agent should use this sub-agent. Write it as a "when" sentence. |
model | model id | none | Decides skill or delegate. See below. |
tools | list of tool names | all tools | Delegates only. The tools the delegate may call. Unknown names are dropped with a warning. |
Skill or delegate. A sub-agent becomes a delegate only when its model: differs from the Hub's model on the same engine, for example claude-local/opus in a claude-local Hub, or a different LiteLLM id in a LiteLLM Hub. A delegate runs on its own model in its own context and returns its answer to the main agent, which stays in control. On the OpenAI Agents backend the main agent calls it as a tool named handover_<name>. On claude-local it runs as a Claude subagent. Every other sub-agent (no model:, the same model, or a different engine) is loaded inline as a skill, and its tools: list is ignored with a warning. A delegate whose model is missing its provider key falls back to a skill so the Hub still starts.
Skills: skills/
Each skill is skills/<name>/SKILL.md (also skill.md, Skill.md or the first *.md in the folder) or a single file, skills/<name>.md.
---
name: weekly-summary
description: Three-bullet summary of the week's orders for the operations lead.
---
1. Call list_knowledge and read the reporting policy.
2. Pull the week's numbers with the stock and order tools.
3. Reply with three bullets and one risk to watch.| Key | Type | Default | Effect |
|---|---|---|---|
name | string | the folder name (or file stem) when missing | Name passed to load_skill. |
description | string | Skill loaded from <file>. when missing | Shown in the skills menu the agent sees. |
The agent sees each skill's name and description and loads the body with load_skill only when it needs it. Other frontmatter keys are ignored. Supporting files in a skill folder are not loaded automatically. Name them by their path from the Hub root in the skill body, and the agent reads them with read_file.
Name collisions resolve in this order: skills/ first, then sub-agents loaded as skills, then Hubzoid's built-in dashboard skill (chat surfaces only). A skill file that cannot be parsed stops the Hub from building.
Knowledge: knowledge/
Every *.md under knowledge/, including subfolders, is one document. Hidden files and _index.md are skipped.
---
name: returns-policy
description: Return windows, exceptions and who approves refunds.
keywords: [returns, refunds, exchange]
---
Returns are accepted within 14 days with the original receipt.| Key | Type | Default | Effect |
|---|---|---|---|
name | string | the file stem | Name passed to read_knowledge. |
description | string | Knowledge document: <stem>. | Shown in the knowledge menu. |
keywords | list of strings | none | Search hints. A single string is accepted. |
The knowledge tools read the folder live, so an edited or added document is visible on the next call without a restart. A file with broken frontmatter is skipped with a warning and the rest still load. Documents saved with the remember tool live in knowledge/_learned/ with the name learned/<topic>. See memory and history.
Tools: tools_local/ and restricted/
Every *.py file in these folders is imported when the Hub builds, and every module-level FunctionTool (a function decorated with @function_tool from the agents package) becomes a tool. Files whose names start with _ are skipped.
- A
tools_local/tool with the same name as a built-in tool replaces the built-in. - A
restricted/<permission>.pytool requires the permission named by the file stem. It wins over any tool of the same name and is guarded before every call. restricted/.envholds the secrets those tools read. File tools refuse every path underrestricted/.
See tools and connectors and restrict tools.
Connectors: connectors/.mcp.json
External MCP servers, in the mcpServers shape. connectors/mcp.json (no leading dot) is read when .mcp.json is absent.
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GH_TOKEN}" }
},
"tickets": {
"transport": "streamable-http",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer ${TICKETS_TOKEN}" }
}
}
}| Key | Applies to | Effect |
|---|---|---|
command, args, env | stdio servers | Process to start and its arguments and environment. |
url | HTTP servers | Server endpoint. |
transport | HTTP servers | sse or streamable-http (also written http). A server with a url and no transport uses SSE. A server with no url uses stdio. |
headers | HTTP servers | Request headers, such as an authorization token. |
client_session_timeout_seconds | HTTP servers | Per-call timeout on the OpenAI Agents backend. |
${NAME} in any string is replaced with that environment variable after .env is loaded. Names use uppercase letters, digits and underscores. With HUBZOID_BROWSER=true, a playwright server is added automatically, and an entry of your own with that name replaces it.
Scheduled tasks: schedule/
One Markdown file per task. The file name (letters, digits, ., _ and -, starting with a letter or digit) is the task name. The body is the agent's instructions, or optional notes for a run: script task.
| Key | Type | Default | Effect |
|---|---|---|---|
schedule | 5-field cron | none | When the task runs, in the machine's local time. |
on_webhook | string or true | none | Run when events arrive at this webhook inbox instead of on a cron. true means webhook. Use schedule or on_webhook, not both. |
run | string or list | none | Run a command instead of the agent. A string runs in a shell, a list runs as arguments without one. |
model | model id | the Hub's model | Model for this task. |
timeout | seconds | 1800 | Time limit per round. |
max_rounds | integer | 10 | Fresh-context rounds per run. |
max_turns | integer | 40 | Agent turns within one round. |
write | path or list | none | Extra Hub paths the task may write, not committed. |
commit | path or list | none | Hub paths the task may write and Hubzoid commits after the run. |
push | boolean | false | Push the commit. Needs commit. |
enabled | boolean | true | false keeps the file but stops it firing. |
The full behaviour, including rounds, catch-up and webhook inboxes, is in markdown tasks.
Workflows: workflows/
Each code workflow lives in workflows/<name>/. Every *.py file there whose name does not start with _ is imported, and each function decorated with @workflow is registered under its function name. hubzoid new workflow scaffolds one. The API is in the workflow API reference.
workflows/settings.yaml is optional. Workflows read any top-level key with hub.setting("<key>"), and two keys configure the engine:
agent_max_attempts: 1 # tries for a failed hub.call_agent step. 1 means no retry.
max_concurrent_workflows: 2 # hub-wide cap on code workflows running at once. Unset means no cap.
stock_drift:
threshold_pct: 5Evals: evals/
One Markdown file per case. The file name is the case name. Files starting with _ or . are skipped. Frontmatter keys are schedule, tags, expect_tools, forbid_tools, contains, not_contains, timeout, threshold and enabled, and any other key is an error. The body holds a ## Prompt section and an optional ## Criteria section. See evals.
Identity: identity/
identity/access.csv
A roster of people. The header row names the columns. phone, email and groups have meaning to Hubzoid, and any other column is kept as context on the person's record.
phone,email,groups,center
919800000001,meera@example.com,coordinator,north
919800000002,arjun@example.com,coordinator;finance,southphoneidentifies WhatsApp and Telegram senders. Formatting is normalized, so a prettified number matches.emaillinks the row to the person's web chat and MCP identity, and is lowercased.groupsare separated by;or,. Rows that share an email combine their groups.
The file reloads when it changes on disk, with no restart. An unreadable or half-written file resolves to no groups, never to old ones, so save it by writing a new file and renaming it into place. On WhatsApp and Telegram the roster is the allowlist: a sender whose number is not listed is refused. For web chat and MCP callers the roster only adds groups.
identity/access.py
A function-backed roster for live lookups, such as a CRM. It wins over access.csv when both exist.
def resolve(surface: str, handle: str) -> dict | None:
"""Return {"email": ..., "groups": [...]} for a phone or chat handle, or None."""
...
def groups_for_email(email: str) -> list[str]:
"""Optional. Groups for a web chat or MCP caller, looked up by email."""
...groups_for_email is optional, and without it the file never receives email lookups. Results of groups_for_email are cached for 60 seconds. An exception in either function denies rather than failing the request.
identity/permissions.yaml
Labels for the permissions the Console shows. The Console lists use_hub, manage_access and one permission per restricted/*.py file, and this file adds text to them without running any tool code.
finance:
label: Finance reports
description: Read margins and receivables from the accounting system.
sensitive: true| Key | Type | Default | Effect |
|---|---|---|---|
label | string | Use this agent for use_hub, Manage access for manage_access, otherwise the name in title case | Name shown in the Console. |
description | string | empty | Explanation shown with the permission. |
sensitive | boolean | false | Tags the permission as sensitive in the Console, and the review step shows a warning before it is granted. |
Access concepts are covered in identity and access.
Branding: branding/
Files copied into the web chat on every start. Names match without regard to case.
| File | Used for |
|---|---|
favicon.svg, favicon.png, favicon.ico | Brand mark in the tab and sidebar. |
logo.svg, logo.png | Used as favicon.svg or favicon.png when that file is absent. |
favicon-dark.png, favicon-96x96.png, apple-touch-icon.png, logo.png, splash.png, splash-dark.png, web-app-manifest-192x192.png, web-app-manifest-512x512.png | Individual slots. Any PNG slot you leave out is filled from favicon.png. |
custom.css | Replaces Hubzoid's baseline stylesheet for the web chat. |
When branding/ holds any file, Open WebUI's own name suffix and branding are replaced, unless HUBZOID_KEEP_OWUI_SUFFIX=true. In a gateway, a Hub's logo or favicon raster (PNG, WebP, JPEG or GIF) becomes its avatar in the model picker.
Open WebUI branding terms
Open WebUI's license allows removing its branding only in specific cases, such as deployments with 50 or fewer users in a 30-day period or with an Open WebUI enterprise license. Read the Open WebUI license (opens in a new tab) before you add files to branding/, and set HUBZOID_KEEP_OWUI_SUFFIX=true to keep Open WebUI's branding.
Runtime folders
Hubzoid creates these while it runs. Keep them out of Git and include them in backups with hubzoid backup.
| Path | Contents |
|---|---|
.hubzoid/hub.db | The Hub's own tables, when SQLite is used. |
.hubzoid/dbos.db | The workflow engine's run history, when SQLite is used. |
.hubzoid/schedule-state.json | Fire times and last results of markdown tasks and scheduled evals. |
.hubzoid/schedule/<task>/ | A markdown task's scratch folder, always writable by that task. |
.hubzoid/chats/<chat>/uploads/, .hubzoid/chats/<chat>/artifacts/ | Files people attached, and files the agent saved with write_artifact. |
.hubzoid/evals/ | One JSON file per eval run. The last 50 are kept. |
.hubzoid/artifact_secret | Secret that signs download links (mode 0600). |
.hubzoid/backups/ | Access snapshots written by hubzoid access migrate --apply. |
.openwebui-data/ | The web chat's database, uploads and log for a single Hub. |
.inbound/webhooks/<name>/ | Webhook events waiting for a task. Handled events move to .processed/. |
.inbound/ | Delivery dedup records and Telegram bindings. |
output/<session>/ | Fallback folder for tool output when no chat is in scope, such as hubzoid test. |
Next steps
Configuration reference
Every environment variable Hubzoid 1.0.1 reads, grouped by area, with its type, default and effect, and how the .env files are loaded.
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.
