Bundles — .botz packaged workflows
A bundle is a deterministic ZIP archive that ships a workflow (main.bot) alongside the resources it depends on — Claude Code skills, reusable prompts, default attachments, a manifest (legacy tar.gz bundles are still read for back-compat). The result is a single .botz file you can email, commit, or drop into S3, and that any iterion install can run with one command.
iterion bots create my-bot # scaffold
$EDITOR bots/my-bot/main.bot # write your workflow
iterion bundle pack bots/my-bot # → my-bot.botz
iterion run my-bot.botz # run itWhy bundles
A plain .bot is one file. As soon as the workflow needs adjacent resources — a project-local Claude Code skill, a reviewer prompt, sample input PDFs — those files have to live on every machine the workflow runs on. Bundles solve that: everything ships together, with a stable content hash that lets two machines extracting the same bundle reuse the same cache slot.
Bundles are also the unit of distribution we expect for shared workflows (templates, examples, organisation-internal recipes).
Quick start
# 1. Scaffold a layout under ./bots/my-bot.
iterion bots create my-bot
# 2. Edit main.bot, drop skills/prompts/attachments as needed.
$EDITOR bots/my-bot/main.bot
echo "# my skill" > bots/my-bot/skills/probe.md
echo "Hello {{vars.topic}}" > bots/my-bot/prompts/helper.md
# 3. Build the deterministic archive.
iterion bundle pack bots/my-bot
# → my-bot.botz (next to the source dir)
# 4. Run it like any workflow file.
iterion run my-bot.botz
iterion run my-bot.botz --preset quick # named preset from main.botLayout
my-bot/
├── main.bot # required — the workflow source
├── manifest.yaml # optional
├── README.md # optional, for human readers
├── skills/ # optional — Claude Code skills
│ └── probe.md
├── prompts/ # optional — reusable .md prompts
│ └── helper.md
├── attachments/ # optional — default values for `attachments:` block
│ └── logo.png
└── presets/ # optional — file-based presets ("sous-bots")
└── sre.md| Entry | Purpose |
|---|---|
main.bot | The workflow source. Must live at the bundle root. |
manifest.yaml | Bundle metadata (name, version, schema_version, optional attachments: map). Optional. |
skills/ | Claude Code skills. Mirrored into <workDir>/.claude/skills/ at run time. Workspace files always win on collision (warn-logged). |
prompts/ | Reusable .md prompts. Each file is auto-registered with name equal to the filename stem — prompts/helper.md makes system: helper resolvable from main.bot. Workflow-declared prompts always win on collision. |
attachments/ | Default binary inputs the manifest can map to declared attachments: entries. Runtime uploads (Launch modal, cloud) override these. |
presets/ | File-based presets ("sous-bots"): each presets/<name>.md (YAML frontmatter + markdown body) is a named launch-time specialization selected with --preset <name>, layering variable overrides + a system-prompt bias + skill hints onto the bot. |
Manifest schema
name: my-bot # human-friendly identifier (display only)
version: 0.1.0 # free-form, semver recommended
description: One-liner.
author: Your Name <you@example.com>
schema_version: 1 # required; iterion refuses unknown versions
# Optional: map workflow attachment names → files inside attachments/
attachments:
logo: branding/logo.png
spec: docs/spec.pdf
# Reserved for future minor extensions (additive). Unknown keys are
# tolerated under `compat:` so newer bundles don't break older iterion.
compat:
some-future-key: …The current schema version is 1. Bundles that omit schema_version are treated as v1. iterion refuses any other value with an explicit upgrade hint.
Determinism
iterion bundle pack produces a reproducible archive:
- entries sorted alphabetically;
- every ZIP entry stamped with a fixed modtime (1980-01-01,
zipEpoch); - modes normalised (
0o644for files,0o755for dirs); - compression pinned to
zip.Deflate.
iterion bundle pack my-bot -o a.botz
iterion bundle pack my-bot -o b.botz
sha256sum a.botz b.botz
# 03551558… a.botz
# 03551558… b.botz ← identicalThis matters because a container-independent SHA-256 over the sorted (path, file-bytes) pairs is the cache key the consumer side uses to look up the extraction slot at ~/.cache/iterion/bundles/<first-2>/<full-hash>/ (Windows truncates the slot name to 16 chars to stay under MAX_PATH). The digest ignores the container format, so a ZIP and a legacy tar.gz of the same files share a cache slot. Two machines packing the same source produce the same hash → cache hits become trivially shareable (e.g. via a CDN that serves the archive but lets each machine extract locally).
Resource resolution at run time
When iterion run my.botz (or a directory bundle) executes:
Skills in
skills/are copied into<workDir>/.claude/skills/with marker-aware collision handling (<workDir>/.claude/skills/.iterion-managed/<name>.sha256records the hash of each file we last mirrored):- File doesn't exist → copy, record marker.
- File exists & content matches source → no-op (already current).
- File exists & content matches marker → refresh from source (we wrote it last, user hasn't customised — fixes the v0.1.0-shadows-bundle-upgrade trap).
- File exists & content matches neither → SHADOW with a warning (genuine user customisation OR a different bundle owns the name; "workspace wins" contract preserved).
Bundle skills mirror first, so on a name collision a bundle skill wins over a plugin skill and a skill-library skill (precedence: bundle > plugin > library > hand-authored — ADR-059).
Prompts in
prompts/*.mdare merged into the ASTprompts:table before static validation runs, so node-levelsystem:/user:references against bundle filenames type-check. Workflow-declared prompts always win on collision.Attachments listed in
manifest.yaml'sattachments:map are promoted viastore.WriteAttachmentbefore the host's attachment-promote callback, so a runtime upload of the same name overrides the bundle default.Sandbox: when active, the bundle directory is bind-mounted read-only at
/run/iterion/bundle(parallel to/run/iterion/attachments). Resources stay reachable from inside the container even though the cache slot lives outside the workspace mount.
Cache & resume
Bundles are extracted once, content-addressed by hash. The slot is marked ready with a .ready sentinel for atomic concurrent extraction and carries a bundle.lock recording the full hash + original archive path.
- Cache hit:
iterion run my.botzreuses~/.cache/iterion/bundles/<hash>/immediately. - Cache miss / GC: iterion re-extracts from
BundlePathrecorded on the run. - Cache + source both gone: resume fails with a clear hint pointing at the archive to re-supply.
iterion resume --run-id <id> re-opens the bundle from the run's persisted BundlePath automatically — the user doesn't re-type --preset or paths, the engine pulls them from run.json.
CLI reference
iterion bots create <slug> Scaffold a bundle source layout.
iterion bundle pack <dir> [-o file] Build a deterministic .botz from a dir.
[--force] Overwrite the output if it exists.
iterion validate <bundle.botz|dir> Validate a bundle and its workflow.
iterion run <bundle.botz> [--preset] Run a workflow from a bundle.
iterion resume --run-id <id> Resume a bundle-launched run.Files the packer skips
The packer ignores patterns that are never useful inside a bundle and that would defeat determinism:
.git/— version control noise..iterion/— local run store of past iterion runs.*.botz— prior builds (avoids accidental nested packaging)..DS_Store,*.swp,*~— OS/editor scratch.
Symlinks, devices, sockets, and other non-regular entries are rejected at pack time with a clear error.
Troubleshooting
bundle: re-extract <path> required (cache miss; original archive absent) The cache slot was purged and BundlePath no longer resolves on disk. Re-supply the archive (or rebuild from source with iterion bundle pack).
bundle: schema_version N not supported by this iterion build The bundle was produced by a newer iterion. Either upgrade your iterion install or downgrade the bundle (set schema_version: 1 in manifest.yaml).
bundle skill "X" shadowed by existing workspace entry A skill with the same name already exists at <workDir>/.claude/skills/. The workspace copy wins — rename either to disambiguate.
bundle/pack: symlinks not allowed The packer refuses symlinks to keep the archive content-stable. Move the target into the bundle tree (or copy it explicitly), or use the filesystem outside the bundle if it's a host-specific resource.
