Heinzelmännchen¶
Your Heinzelmännchen are what hz is: one for each contract you have, yours or your team's; hz heinzel
list shows them, and the web app's Heinzelmännchen page too. A contract keeps one thing true: "keep this asset true, given these inputs". It is a <name>.prose.md file whose
YAML frontmatter says what it reads and when it runs, and whose Markdown body says what the asset must be. You write
it, hz heinzel check checks it, hz heinzel wake drafts it, and your daemon drafts it on its schedule. Drafts are
saved locally; nothing is published by itself.
There are no workflow files. A workflow is what your contracts make together: a contract that reads another's
output depends on it, and hz heinzel graph draws the graph those inputs and outputs form. To add a step, add a
contract. A contract runs on its schedule or when you run it; later, also when one of its inputs changes.
This page is the journey from nothing to a scheduled contract, then the reference. The decisions behind it are in ADR 0021; every command and option is in CLI_REFERENCE.md, every setting in CONFIGURATION.md.
Personal conversations¶
A personal Heinzel can have a durable identity, name and look before it has a contract. Create one with a stable
request id, then use the returned session_id to reopen it or change its appearance:
hz heinzel new-chat create-helper --name "My helper" --appearance '{"hat":"blue"}' --json
hz heinzel sessions --json
hz heinzel session <session-id> --json
hz heinzel profile <session-id> change-look 1 --appearance '{"hat":"teal","accessories":["scarf"]}' --json
Profile changes name the revision displayed in the snapshot. If another change arrived first, reload before saving. Retrying an identical request id and payload returns its durable result; changing that payload is refused. Omitted personal fields inherit the renderer's defaults, including theme colours. To restore those defaults, send an empty appearance object. The web app's Heinzelmännchen page lets you create a personal Heinzel, choose its name and look, and preview it with the active theme. After saving, continue to its chat to propose a check-in schedule; profile editing alone does not start a schedule. A profile update does not replace an already queued message or approve a contract.
Scheduled agent execution proposal¶
You can record an inert proposal for a possible future scheduled model turn with
hz heinzel propose-scheduled-agent <session-id> <request-id> <contract> <max-usd> <timeout-seconds>.
Read it with hz heinzel scheduled-agent-proposal <session-id> <request-id> --json to inspect the exact contract,
proposed per-turn budget, time limit, and empty permitted-tools list. The preview hash identifies those terms; it is
not an approval, and it does not bind an owner decision or schedule revision. The limits are not yet enforced and no separate approval or scheduled agent execution exists. The
existing notification-only check-in contract is unaffected.
Five-minute check-in¶
The first scheduled personal Heinzel stores one immutable versioned check-in contract and its five-minute schedule in the same private control journal as its identity. Proposing it does not start work. Inspect the returned contract and schedule. Pause is available directly; activation and resume require trusted human approval of the exact schedule intent. The web chat flow is one way to provide that approval. The schedule revision, contract version and preview hash are bound into the short-lived proof issued after confirmation.
hz heinzel propose-check-in <session-id> check-in-v1 --timezone Europe/Berlin --json
hz heinzel schedule-control <session-id> pause-v1 2 pause --json
The web conversation panel offers “Review activation in chat” for a proposed schedule. The stored proposal shows the
exact change before the person confirms it. That chat confirmation invokes the internal issuer and passes a short-lived
proof directly to schedule control. Direct stdio MCP activation and resume show the exact schedule arguments on the
controlling terminal and require a fresh typed challenge; the tool schema accepts no proof, and the proof stays inside
the server. If stdio MCP has no controlling terminal, that direct action fails closed; use paired web review or the
private CLI/web handoff to authorize the chat-scoped MCP confirmation tool. MCP over HTTP cannot access private
schedule controls. The CLI reads a proof from hidden input or stdin with --approval-proof - and never expects the
secret as a command-line argument.
An active check-in attempts one local system.notify notice for each eligible five-minute slot after activation or
resume. The daemon must tick at least every five minutes; a slower configured or foreground cadence reports a failed
check-in instead of silently skipping slots. The reported next wake remains unknown until runtime timing can be
verified. A manual daemon wake does not run this scheduled-only check-in. The notice and its replay key are committed
atomically; the replay key stays until the owner's outbox records completion, so a retry after restart recognizes it
even if the notice was pruned from the display list. Completed keys are removed while unresolved intents keep theirs.
A failed notice is retried twice within its tick, then recorded as failed; that slot is not retried on later ticks. A
damaged durable notification store must be restored before new notices can be recorded. This replay guarantee assumes
all concurrent notification writers use the durable-key format; stop older Heinzel processes before upgrading while
they still write the same notification store. The owner's control journal remains append-only and retains completed
slot history. Creating a personal Heinzel never installs or enables background work or requests notification
permission. This first slice does not run an agent model turn or execute user-authored contract text.
The outbox migrates earlier append-only check-in.intent and check-in.recorded journal records while holding the
same per-session SessionStore control lock used by those writers. This migration is safe for older writers that use
that lock. The last published version before check-in support, v0.7.27, uses the same per-session control lock; the
published release tags do not contain the legacy check-in writer, so the append-only records being migrated came from
pre-release builds. If a daemon unit is installed, schedule approval requires its version marker and the version
recorded by the last service-managed daemon tick to match the current install. A tick records its version only when
launchd or systemd reports the current process as the service's active PID; manual CLI or MCP ticks cannot establish
that proof. The tick version stays unknown for older daemons that do not record it, and remains unknown after restart
until a service-managed tick runs; approval fails closed while an installed daemon's version is unknown or mismatched.
If no daemon unit is installed, activation can preconfigure the schedule, but it remains dormant and sends no notices
until a daemon is installed, started and observed running the current version. The install marker and service-manager
reload result are not treated as proof that the new code ran. The tick version records the code that executed in that
invocation, not proof about a separate manually launched process.
From the CLI, use hz system daemon status --json to inspect installed, loaded, paused, interval_minutes,
version, installed_version and version_mismatch. An absent daemon leaves this schedule dormant; install one with
hz system daemon install --interval 5m only if you want the persistent login service. A paused daemon can be resumed
with hz system daemon resume. If the service is unloaded, or its tick version is unknown or mismatched, run
hz system daemon restart and wait for version to match installed_version before relying on check-ins. A manual
hz system daemon tick does not provide that service-managed version proof. Five-minute schedules require an interval
of five minutes or less; reinstall with --interval 5m if the reported cadence is slower.
Advisory file locks cannot fence a process that appends without taking the lock, and this journal has no writer epoch or compare-and-swap append to stop one. Stop any such process before enabling scheduled check-ins; concurrent unlocked writes are unsupported and can leave migration state ambiguous.
hz heinzel chat <session-id> <request-id> <message> --json currently acknowledges a durable inbox entry. A queued
receipt confirms acceptance, not a completed model response. The same request_id appears on the user message,
so a lost network acknowledgement can be reconciled without submitting another message. Session snapshots page
through an opaque cursor and stay readable while a turn is running.
The web composer also recognizes a narrow fixed request for a check-in every five minutes. It prepares the standard schedule preview and opens the existing exact-intent approval card; only the person's separate confirmation activates it. This local phrase match does not call a model or infer other schedule changes. Other messages remain queued, and general natural-language replies still require a future conversation runner.
The personal session journal does not yet contain an owner-bound agent activity or tool-call timeline. Newly opened check-in runner chats record the authenticated owner and personal session, and reject another owner on lookup. A personal session reuses its bound chat for subsequent opens and schedule review; generic chat retention does not prune it. Older unbound chats or multiple chats claiming one personal session fail closed and require inspection. The chat still has bounded message and proposal counts. It records runner-reported aggregate tool counts, not a durable ordered activity log for a personal Heinzel. An activity feed requires a trusted runner-to-session link and bounded redacted records; client-authored messages must never be interpreted as tool events.
These operations are private: local CLI, stdio MCP, paired web and the authenticated private API use the same owner and service. The unauthenticated REST API and network MCP do not expose them. Names, appearance and message text live in hash-verified private bodies; the durable journals contain opaque references and revisions.
What happens to a contract¶
flowchart LR
You(["you, or your team's repository"]) -- "<name>.prose.md" --> Check["hz heinzel check"]
Check --> Run["hz heinzel wake<br/>or the daemon on its schedule"]
Run -- "inputs, through capabilities" --> Agent["one bounded agent session<br/>(no hz tools)"]
Agent -- "draft + .record.json" --> Drafts["hz heinzel drafts"]
Drafts -. "hz inbox (later)" .-> Publish["publish operation"]
- Found: contracts are read from your workflows directory (
me) and, when configured, fromworkflows/in a checkout of your team's state repository (team).hz heinzel listshows both directories. - Checked: the frontmatter against the schema, the body against the required sections, and both against the
installed modules. Every problem has a file, a line, a stable
HZCcode and a hint. - Rendered: the inputs are fetched through the capabilities the contract names (never vendors; configuration binds an adapter to each), then one agent session runs with read-only tools and gets the body as its instructions and the inputs as data. It never gets hz tools.
- Saved: the draft and a
.record.json(the packet, each input's hash, the agent's report) land under<data dir>/workflow/drafts/<namespace>-<name>/. Nothing is published:publishesnames where the draft is meant to go, andhz inbox(not built yet) will ask you first.
The journey¶
First contact¶
hz heinzel list # what a contract is, the directories searched (and whether they exist yet), what to run
hz heinzel check # every problem by file and line; the same empty state when there is nothing yet
Write your first contract¶
hz heinzel new weekly-digest
This writes weekly-digest.prose.md into your workflows directory (creating it) and prints what to do next. The
starter is valid as written and commented: every key and section says what it is for. It declares no inputs, so it
checks and dry-runs on a fresh installation, where only the agent runner is bound; inputs, requirements, publishes and
the schedule are commented examples to uncomment as you go. A name must be lowercase words joined by dashes; the
error for anything else shows the name that works.
To start from a complete contract instead, use a plugin's playbook: hz heinzel playbooks lists the template
contracts installed plugins offer, and hz heinzel new <name> --template <plugin>:<template> writes one. The
walkthrough, with a weekly report as the example: Playbooks: your first contract from a plugin.
Edit it in any editor, then:
hz heinzel check --path ~/path/to/weekly-digest.prose.md # after each edit
hz heinzel show weekly-digest # is it ready? what does it require?
Try it without spending¶
hz heinzel wake weekly-digest --dry-run
A dry run fetches the inputs and prints the packet exactly as the agent would get it: the instructions (the body) and the prompt with the inputs as data. No agent runs and nothing is spent. When the packet reads right:
hz heinzel wake weekly-digest # drafts it now; at most `[heinzel] max_usd` (default 1 USD) or the contract's own cap
Bind what it requires¶
hz heinzel show <name> lists every requirement with the adapter bound to it, and a command for each unmet one:
- an installed adapter:
hz system config set capabilities.tracker <adapter>; - none installed:
hz system plugin search --text trackerfinds a plugin that provides the capability.
hz system capability list shows every capability, adapter and binding. The agent runner is a capability too
(agent-runner, bound to claude-code by default; hz system doctor checks it).
Schedule it¶
Uncomment schedule: in the frontmatter (five cron fields in local time, e.g. "0 7 * * MON"). hz heinzel show
then names the daemon job, workflow:me-<name>, and:
hz system daemon status # is the daemon installed? If not:
hz system daemon install # launchd or systemd runs `hz system daemon tick` every 5 minutes
hz system daemon tick --job workflow:me-weekly-digest # run it now as the daemon would, with a receipt
The daemon reads contracts afresh on every tick, so an edit counts at the next one. A contract without a schedule runs only by hand. See ADR 0020 and SECURITY.md for what runs unattended.
Find and read your drafts¶
hz heinzel drafts # every draft, newest first, with its path and record
hz heinzel drafts --name weekly-digest --latest # the newest draft of one contract, as text
A manual run has no receipt, only its record; daemon runs write a receipt that names the draft and its record and holds the draft's hash, the input hashes and the agent's report, but not the draft's text.
How long drafts are kept¶
Drafts and their records (which hold the fetched inputs) are deleted 90 days after their render; receipts are kept.
hz heinzel prune --dry-run # what would be deleted now, deleting nothing
hz heinzel prune # delete it; running it again deletes nothing more
hz system config set heinzel.retention_days 30 # keep them 30 days; 0 keeps them forever
The daemon runs the job heinzel:prune daily, so with the daemon installed you don't run it yourself; hz system
doctor says how much would be deleted, and degrades when expired drafts are more than two days overdue. Reading drafts
never deletes them. Only files a render wrote below <data dir>/workflow/drafts/ are deleted, never through a symbolic
link. Receipts the daemon wrote before this change, with 0.2.0, hold the draft's text too; they are never rewritten,
since that would break their chain. To drop them, move that job's receipts out of <data dir>/receipts/, which starts
its chain again (ADR 0020).
Measure whether it pays off¶
hz heinzel sent weekly-digest --file ~/sent/digest.md --edit-minutes 5 # how much of the newest draft you changed
hz heinzel sent weekly-digest --ref <document> # or the approved version where it lives, through `documents`
hz heinzel show weekly-digest # the success test: drafting time, share changed
sent compares what you finally sent with the draft and saves the share of words changed, both word counts and the
sent text's hash next to the draft; the sent text isn't copied. show lists each draft's drafting time, cost and
change. It stays local and is about the draft, never about people.
Contribute a contract to your team's repository¶
hz team join https://github.com/acme-example/hz-team-payments.git --yes # or: hz team adopt /path/to/checkout
hz heinzel new retro-agenda --team # writes <checkout>/workflows/retro-agenda.prose.md
hz team join clones your team's repository and makes it your team; hz team new creates one (TEAMS).
[heinzel] team_repository still works but is deprecated: hz team adopt <its path> moves it to [team], and
hz system doctor says so until you do.
Commit it on a branch and open a pull request; the repository's CI can gate with hz heinzel check --path workflows
(it exits with 1 on errors). Once merged, everyone with the checkout sees it as team:retro-agenda and can run it by
hand. It runs on your daemon only when you say so:
hz heinzel show team:retro-agenda # prints the SHA-256 and the approval entry
hz system config set heinzel.schedule_team '["retro-agenda"]' # run whatever the file says
hz system config set heinzel.schedule_team '["retro-agenda@sha256:<hex>"]' # run exactly this content
A pinned contract whose file changed doesn't run, and hz system doctor says so until you approve the new content.
Link contracts into a workflow¶
A contract can read another contract's output, by namespace, name and output:
hz:
inputs:
digest: {read: "contract:me/weekly-digest#digest", stage: draft}
It gets the newest draft of that output, as text (or JSON for a json output). stage: draft is required: nothing
is published yet, so drafts are all there is. Then see the workflow your contracts make:
hz heinzel graph # each contract, what it maintains and what it reads
hz heinzel graph --mermaid # the same as a Mermaid flowchart, for a document or a pull request
hz heinzel graph --json # nodes, edges, cycles and inputs nobody maintains
A contract that depends on itself, directly or through others, is an error and doesn't run (HZC030). An input that
reads an output no contract maintains is a warning (HZC031), and that contract's runs fail until someone writes it.
A team contract can't read a personal one's output (HZC032).
Fix an error¶
hz heinzel check prints every problem as file:line CODE field message with a hint on the next line; show and
run repeat a contract's errors and name the check command that rechecks it. The codes:
| Code | Means |
|---|---|
HZC001 |
The file or directory can't be read, or has no frontmatter |
HZC002 |
A frontmatter key is unknown, missing or invalid; the hint names the schema |
HZC003 |
The frontmatter isn't YAML |
HZC004 |
YAML the loader refuses: anchors, tags, duplicate keys, too deep or too large |
HZC005 |
Control characters in the file |
HZC010 |
The file name and name disagree |
HZC011 |
The schedule can't be compiled into a daemon job |
HZC012 |
A publishes operation isn't provided by an enabled module (warning) |
HZC013 |
A publishes operation is read-only, so it can't publish |
HZC023 |
A key or section ADR 0021 names that this version doesn't build yet |
HZC024 |
A tool the bound runner doesn't accept as read-only (warning) |
HZC025 |
A playbook's {{asset:<type>}} placeholder is left in an input's arguments; name your asset there |
HZC030 |
The contract is on a cycle: it depends on itself, directly or through others |
HZC031 |
An input reads an output no contract maintains (warning) |
HZC032 |
A team contract reads a personal contract's output |
HZC101, HZC107 |
### Goal or ### Done when missing or empty |
HZC103 |
The items under ### Requires or ### Maintains don't match hz.inputs or hz.outputs |
HZC105 |
An unknown section (warning: the agent sees it as part of the contract) |
HZC106 |
A retired OpenProse section |
Reference¶
The file¶
<name>.prose.md: YAML frontmatter between --- lines, then a Markdown body. name equals the file stem. UTF-8, at
most 256 KiB. HTML comments in the body are authors' notes; the agent never sees them. The machine-readable schema of
the frontmatter is schemas/contract-frontmatter-1.json. The example the tests
use: tests/fixtures/workflows/weekly-digest.prose.md.
Frontmatter¶
The top level is OpenProse-compatible; everything Heinzelmännchen reads is under hz:.
| Key | Required | Meaning |
|---|---|---|
name |
yes | Lowercase words joined by single dashes, at most 64 characters; equals the file stem |
kind |
yes | responsibility |
description |
yes | What the workflow keeps true, and for whom (at most 1024 characters) |
version, id |
no | The author's semantic version; OpenProse's minted id |
x-* |
no | Ignored, for other tools |
hz.schema |
yes | 1 |
hz.title |
yes | Display name, at most 80 characters; the daemon job's summary |
hz.schedule |
no | Five cron fields in local time, or @daily and friends; without one the contract runs only by hand |
hz.requires |
no | Capabilities read, <capability>:read (e.g. tracker:read); agent-runner:read is implied; never an adapter, never write |
hz.inputs |
no | By name: read (<capability>.<method>, e.g. tracker.issues_updated), args (the method's arguments), window (for methods that take one: 7d or last 7d back from the run, next 7d ahead of it, e.g. a calendar's coming week; at most 366d). Each needs its capability in hz.requires. Or read: "contract:<namespace>/<name>#<output>" with stage: draft: another contract's newest draft, no capability needed |
hz.outputs |
yes | Exactly one, by name: format: markdown (default) or json |
hz.publishes |
no | Where the draft is meant to go: action (a publish operation, <module>.<operation>) and output; recorded, never run by itself |
hz.agent.tools |
no | Read-only tools the agent may use, in the runner's naming; empty for the runner's default |
hz.agent.max_usd |
no | Most one run may cost; [heinzel] max_usd caps it whatever it says |
hz.timeout |
no | The agent session's limit, default 10m, at most 2h |
hz.retries |
no | 0 to 3 retries within a daemon tick; they share the cost cap |
hz.catch_up |
no | Missed slots: latest (default) or skip |
hz.scope |
no | personal (the only value yet) |
Keys ADR 0021 names for later (owner, tags, freshness, on, tools, concurrency, cost, overrides,
scope: team, output targets, input sources and more) are refused with HZC023 and a message saying what comes
instead. The runner and model are never in a contract: they are configuration ([capabilities] agent_runner,
[system.runner], ADR 0029).
Body¶
| Section | Required | Content |
|---|---|---|
### Goal |
yes | What is true once the draft is accepted |
### Requires |
with inputs | One - `name`: ... item per key of hz.inputs |
### Maintains |
yes | One - `name`: ... item per key of hz.outputs: what the output must be |
### Done when |
yes | The postconditions, one - [ ] ... per line; the agent checks them before answering |
### Steps, ### Continuity, ### Invariants, ### Description |
no | Guidance, what carries over between runs, what must always hold, context |
The whole body, minus comments, is the agent's system prompt. The prompt then names the output to draft and carries the
inputs as data in <input> elements, framed as never being instructions.
Where things are¶
| What | Where | Setting |
|---|---|---|
Your contracts (me) |
<user config dir>/workflows/ (hz heinzel list prints it) |
[heinzel] user_dir |
Team contracts (team) |
<team checkout>/workflows/ |
[team] checkout (hz team join), [heinzel] schedule_team |
| Drafts and records | <data dir>/workflow/drafts/<namespace>-<name>/, kept 90 days by default |
[hz] data_dir, [heinzel] retention_days |
| Cost cap | 1 USD a run by default, in every head | [heinzel] max_usd |
A project hz.toml can't set any [heinzel] key: a repository someone else controls must not decide which prompts
your daemon runs or what they cost; hz system doctor names the keys it ignored. Symbolic links are never followed, in
the directories or for drafts. A bare name that exists in both namespaces is an error that asks for me: or team:.
What 0.2 does not do yet¶
No hz inbox and no publishing, so contract inputs read drafts; no fingerprints or memo keys; no input wakes (on);
no team scope with leases, no embedded contracts, no hz heinzel migrate. ADR 0021 lists each with the version it is planned for.