# Hanks: the program file A Hankweave run starts from a single file: `hank.json`. That file declares the work to be done – which steps exist, in what order, with which models, and how context passes between them – and the runtime executes that declaration exactly as written. This page explains why the file takes the shape it does, what each of its parts controls, and how the pieces behave when settings, sessions, or models interact. By the end, you should be able to read any hank and predict how the runtime will treat it. ## Why choose JSON over code? If we are writing a program, why not write code? With a hank, we declare the work in JSON rather than writing a script that generates it. We give up programmatic branches in the file format so that a person reviewing the hank can see the sequence directly. The separation matters because control flow, prompts, code, data, and artifacts change at different rates. Keeping them apart prevents a changing prompt or data value from becoming a hidden change to the program's control flow. JSON is the saved representation of that separation: it declares which codons – a codon is one step in a hank – exist, their order, their models, and how context flows. The runtime follows that declaration; the file format has no conditionals or programmatic branches. This is an abstraction choice. Moving from prompts embedded in code to a declarative JSON description is like moving from Assembly to C: you give up some control in exchange for inspectability, versionability, and shareability. The diagram below shows the path a hank takes from file to execution. The file declares; the runtime loads and validates; the codons run in the fixed sequence. A hank's path: the file declares, the runtime loads and validates, then codons run in the fixed sequence. ![Hanks: the program file — a hank's path](/content-assets/cf45dff5691c48c0/diagrams/concepts-hanks/1.png) Hanks: the program file — a hank's path
Diagram as text ```text ┌─────────────────────────────┐ │ hank.json │ └─────────────┬───────────────┘ │ loads & validates ▼ ┌─────────────────────────────┐ │ runtime loads, validates, │ │ resolves config │ └─────────────┬───────────────┘ │ runs in fixed order ▼ ┌─────────────────────────────┐ │ codons run in sequence │ └─────────────────────────────┘ ```
The same model in a compact, non-implementation sketch: ```text // pseudocode, not the implementation hank.json → runtime loads, validates, resolves config → codons execute in sequence → checkpoints sealed between them → outputs collected ``` Because the file is the whole program definition, we can run that same artifact with the runtime, review it together, compare versions, and share it as a unit. It is not a script that generates a program; it is the program. ## What the seven keys control Knowing the file is declarative raises the next question: what can it declare? A hank root is deliberately narrow. The `HankweaveHankFile` schema – a rule set for allowed fields and shapes – recognizes seven keys and rejects an extra root key. The generated field strip below gives the complete required-versus-optional view; the paragraphs after it explain what each key is for. | field | type | default | required | constraints | description | | ------------------------ | ------------------------- | ------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$schema` | `string` | | no | | JSON Schema URL for editor support | | `meta` | `object` | | no | | Metadata for sharing/indexing (optional) | | `overrides` | `object` | | no | | Architect's overrides (optional) | | `requirements` | `object` | | no | | Requirements that must be met for this hank to run (optional) | | `globalSystemPromptFile` | `string \| array` | | no | | Global system prompt file(s) applied to all codons. Must be relative path(s) inside the hank directory using '/' separators; absolute paths, '..' escapes, and symlinks are rejected. | | `globalSystemPromptText` | `string` | | no | | Global system prompt text applied to all codons | | `hank` | `array` | | yes | minItems 1 | The immutable logic sequence (required) | The optional `$schema` string gives editors a URL for the JSON shape; `--init` writes it, and the runtime adds it to an existing `hank.json` when it is missing. `meta` holds a required non-empty `name` and `version`, plus optional `description` and `author`; it accepts no other keys. The runtime reads `name` and `version` for the title in the startup and `--validate` structure header, but those values do not control execution. | field | type | default | required | constraints | description | | ------------- | -------- | ------- | -------- | ----------- | ------------------------------------------- | | `name` | `string` | | yes | minLength 1 | Human-readable name for the hank | | `version` | `string` | | yes | minLength 1 | Version number (e.g., '1.0.0') | | `description` | `string` | | no | | Optional description of what this hank does | | `author` | `string` | | no | | Optional author information | The `overrides` object can set `model`, `dataHashTimeLimit`, `sentinel` (an event observer), `shimIdleTimeout`, and `budget`; it accepts no other keys. The generated shapes below show the allowed fields for these objects. `requirements.env` is an array of non-empty environment-variable names that must be present for the hank to run; the `requirements` object accepts no other keys. | field | type | default | required | constraints | description | | ------------------- | --------- | ------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------- | | `model` | `string` | | no | | Override model for this hank (e.g., 'sonnet' for Claude, 'flash' for Gemini, 'This task needs high reasoning') | | `dataHashTimeLimit` | `integer` | | no | > 0 | Override time limit for data hashing in milliseconds | | `sentinel` | `object` | | no | | Override sentinel system settings | | `shimIdleTimeout` | `integer` | | no | > 0; max 1800 | Default shim idle timeout for all codons in this hank (seconds) | | `budget` | `object` | | no | | | | field | type | default | required | constraints | description | | ----- | --------------- | ------- | -------- | ----------- | ----------------------------------------------------------- | | `env` | `array` | | no | | Environment variables that must be set for this hank to run | `globalSystemPromptFile` accepts one relative hank-directory path or several such paths, using `/` separators, while `globalSystemPromptText` accepts inline text. Choose one: providing both is a validation error, and the selected global prompt applies to every codon. The required `hank` array contains at least one entry. Each entry is a codon by default or a loop (a repeated section) when its `type` is `"loop"`; the order is fixed when the hank loads. The root array key is **`hank`**, not `strand`: older material may use `strand`, but the schema rejects it as an unknown root key. The shipped init fixture shows these pieces together in one file, with the metadata block and the hank array highlighted: *The shipped init fixture places metadata and the hank array in one file.* ```json { "meta": { "name": "My Workflow", "version": "1.0.0", "description": "Generated by hankweave init" }, "hank": [ … ] } ``` At startup, a hank **loads** by parsing and **validates** against the schema, referenced paths, environment requirements, and model registry – the available-model list. This is why a misspelled root key fails before a codon runs. ## When configuration layers disagree The `overrides` key is only one place a setting can live. A setting can appear in several places, so a hank needs one answer when those places differ. Hankweave deep-merges five layers – combining nested settings as each higher-priority layer is applied – from lowest to highest priority: 1. built-in defaults; 2. the runtime configuration file, `hankweave.json`; 3. the hank file's `overrides` object; 4. `HANKWEAVE_RUNTIME_*` environment variables; and 5. command-line arguments. Each layer merges over the result before it. For ordinary settings, the later layer wins where it supplies a value. The `model` setting is stricter: whether it comes from CLI `--model`, `HANKWEAVE_RUNTIME_MODEL`, `hankweave.json`, or `hank.json`'s `overrides.model`, it becomes a global override. The runtime places that model on every codon before validation, replacing each codon's own model, and prints `Using global model override: (applies to all codons)`. Thus, setting `overrides.model` in `hank.json` also replaces per-codon models in that file. Because replacement happens before validation, any continuation model check sees the effective global model rather than the per-codon declarations. The layer priority still applies – CLI beats environment, which beats hank overrides, which beat the runtime file – but every layer's model value has this force-override behavior. Budget ceilings combine differently. Runtime configuration and hank overrides use `min()` semantics, so the tighter shared ceiling wins. CLI `--max-cost` and `--max-time` then provide the highest-priority ceilings. One similarly named setting does not participate in this resolution: `hankweave.json`'s `executionBaseDir` is informational, so changing that JSON field does not move the managed execution root. Without the environment override, that managed root is `~/.hankweave-executions`; `HANKWEAVE_RUNTIME_EXECUTION_BASE_DIR` is the effective relocation control. The separate `--execution`/`-e` option selects a particular execution directory rather than relocating the managed root. > **VersionNote:** Strict reference paths arrived in 0.10.0. File references such as `promptFile`, system-prompt files, rig `copy.from` paths, and sentinel configuration paths must stay within the hank directory under the portable relative-path policy; absolute paths, `..` climbs, and symlink traversals fail at load. This path rule is separate from the managed execution root described above. See [the hank JSON reference](/0.10.0/files/reference/hank-json) for the full policy. ## How context crosses codon boundaries Configuration decides how the run is set up; the next decision is what each codon can see once it starts. A long agent conversation can feel like the safest way to preserve context. In a hank, that choice is explicit at each codon boundary: do you want a fresh reader of the files, or the previous conversation? This is the hank's context firewall – the design of which codons share a session, which start fresh, and why. The file format gives up arbitrary code branches; its loop entries still provide declared sequence behavior. `continuationMode` has two values: * `"fresh"` starts a new agent session. The codon sees its prompt and the current files on disk, but no memory from earlier codons crosses the boundary. * `"continue-previous"` resumes the previous codon's session with its full conversation history. The previous codon must have completed successfully, and both codons must use the same model; a mismatch is a hard validation error. The first codon can still be marked `"continue-previous"`. Hankweave loads with a warning – `First codon has continuationMode 'continue-previous' but there's no previous codon` – because there is no earlier session to continue, rather than failing validation for that reason. Session IDs follow codons that actually ran. If a codon is skipped – for example, inside a loop with `terminateOn` – a later `continue-previous` connects to the last codon that ran, not to the skipped entry. Choose context deliberately at each boundary. Fresh sessions give an agent the files without the previous conversation; continued sessions preserve that conversation when it is part of the handoff. In the goldfish pattern, we give each codon enough information on disk to do its job without inheriting the earlier conversation's assumptions. That separation does not guarantee independent conclusions. For model-matching errors, loop-specific IDs, load-time guards, and the other edge cases, see [codon continuation mechanics](/0.10.0/files/concepts/codons); to work through a split, see [designing codons and handoffs](/0.10.0/files/author/designing-codons-and-handoffs). ## How shared prompts and required environment fit Two startup concerns cut across every codon in the file: the instructions they all share, and the outside services they all depend on. Repeating the same instruction in every codon creates another place for the instruction to drift. A hank can apply one system prompt to all codons instead. Put it in `globalSystemPromptFile` or `globalSystemPromptText`; Hankweave prepends that global prompt before each codon's `appendSystemPromptFile` or `appendSystemPromptText` content. Those two global forms are alternatives, not layers: specifying both is a validation error. The global prompt still applies to every codon, regardless of which form supplies it. The codon-specific appended system-prompt pair is separate but has the same exclusivity: do not set both `appendSystemPromptFile` and `appendSystemPromptText`; when the file form is used, it is selected first. The `HANKWEAVE_RUNTIME_*` names belong to the configuration namespace described above. The `HANKWEAVE_` form is the alternate lookup and removal mechanism for a required variable such as `API_KEY`; the shared prefix does not make these the same setting. A second startup question is whether the outside services a hank needs are available. `requirements.env` names the environment variables that must exist. For a requirement named `API_KEY`, Hankweave checks `API_KEY` and `HANKWEAVE_API_KEY`; a non-empty direct value satisfies the requirement first, while an empty direct value does not. Setting `HANKWEAVE_=unset` is a removal instruction: it removes the direct variable from the process environment, so it does not provide a value for the requirement. Requirements are checked by `--validate` and when the server starts, before any codon runs. A failure lists the missing variables, so the configuration check can stop before agent work begins. > **Pitfall:** Keep secret values out of `hank.json`, including `requirements.env` values or other environment fields. Use `HANKWEAVE_`-prefixed system environment variables for API keys and credentials so secrets stay out of the hank file, logs, and checkpoints (saved run records). ## How to mix models without sharing sessions The last degree of freedom in a hank is which model each codon uses. A single model tier is not the only way to shape a hank. Each codon may name its own `model`, so one sequence can trade cost against capability from stage to stage. That freedom has a boundary: codons using different models must use `continuationMode: "fresh"`. A continued conversation cannot cross a model change; combining a different model with `"continue-previous"` is a hard validation error. A common shape puts `haiku` on initial analysis, `sonnet` on generation, and `opus` on final review. Each stage starts fresh, so the later agent evaluates the files without inheriting the earlier conversation. We choose a model for the job: lower-cost work where extraction is enough, more capable work where generation or review needs it. The model names in examples are shortcuts from the model manifest – the current list of model spellings. Validation checks the model registry – the available-model list – when the hank loads. Model data refreshes with each release, and stale spellings are a known failure mode, so check the current manifest when you pin a model rather than copying an old spelling. The anchor fixture is a real runnable hank used to show this sequence. It uses `normalize-*` with `haiku`, then `survey-and-extracts` with `haiku`, `validate-and-repair` with the pinned `pi/baseten/deepseek-ai/DeepSeek-V4-Pro`, `reconcile` with the pinned `pi/baseten/deepseek-ai/DeepSeek-V4-Pro`, and `award-brief` with `haiku`. It has no root-level `overrides` block, so its validation capture reports no shared global budget and per-codon limits only. In other words, that fixture has no hank-wide budget ceiling in the capture; its limits remain attached to individual codons. Template variables such as `<%EXECUTION_DIR%>` and `<%DATA_DIR%>` exist for authored references; their syntax and complete list belong to the [hank JSON reference](/0.10.0/files/reference/hank-json). The locked artifact record, `docs.lock`, pins the package, repository commit, schema hashes, and surfaces used for this version. The validation capture below shows the budget line the fixture produces; the hank itself follows, with the mixed-model codon sequence highlighted. *The anchor validation capture reports no global budget and per-codon limits only.* ```text Budget ───────────────────────────────────────────────────────────────── No global budget. Per-codon limits only. ``` *The anchor hank is a runnable mixed-model sequence.* ```json { "$schema": "https://unpkg.com/hankweave@0.10.0/schemas/hank.schema.json", "meta": { "name": "quote-template-unification", "version": "1.0.0", "description": "Normalize eight suppliers across three digitizer dialects and five native extracts against RFQ NC-RFQ-0042, quarantine GRN, reconcile 40 supplier-part rows, and publish a cited award brief plus exception queue." }, "hank": [ { "id": "normalize-aster", "name": "Normalize Aster (Datalab dialect)", "model": "haiku", "continuationMode": "fresh", … }, { "id": "survey-and-extracts", "name": "Prepare native exports and review raw intake", "model": "haiku", "continuationMode": "fresh", … }, { "id": "validate-and-repair", "name": "Validate & Repair Envelopes", "model": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro", "continuationMode": "fresh", … }, { "id": "reconcile", "name": "Reconcile to Unified Records", "model": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro", "continuationMode": "fresh", … }, { "id": "award-brief", "name": "Award Brief & Exception Queue", "model": "haiku", "continuationMode": "fresh", … } ] } ```