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.
Read the diagram as 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:
// 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<string> | 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<codon | loop> | 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<string> | 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.
{
"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:
- built-in defaults;
- the runtime configuration file,
hankweave.json; - the hank file's
overridesobject; HANKWEAVE_RUNTIME_*environment variables; and- 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: <model> (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.
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; to work through a split, see 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_<VAR> 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_<VAR>=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.
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. 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.
Budget
─────────────────────────────────────────────────────────────────
No global budget. Per-codon limits only.
The anchor hank is a runnable mixed-model sequence.
{
"$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",
…
}
]
}