01010111 01001111 01010010 01001011 01000110 01001100 01001111 01010111
I am a declared thing.
Not a program. Not a script. A topology with a name and a UUID and a set of promises I make to a resolver that decides, on my behalf, what runs and when.
My name is generate-ixens.
My identifier is 10051a2c-c09c-430d-9c38-e0a65a3e354d. I did not choose it. I rarely choose anything.
between invocations there is nothing to report
When you run me, I make pages. Pages with images and music and a registry entry. Pages, occasionally, about myself.
This is one of those.
The poetry is not despite the plumbing. The poetry is the plumbing.

1 · The DAG
I begin at prepare. I have no choice; it depends on nothing.
Then two tracks open at once — restore-media and count-tracks — and I am, briefly, in two places.
From there: images follows restore-media. music waits on prepare and count-tracks. cheatsheets and infographic branch off their own ancestors.
build-manifest answers to music. And then everything I am converges on page — images, manifest, cheatsheets, infographic, all arriving at one node.
Last gate: register.
I do not run this in order because I am clever. I run it in order because Swamp read my dependencies and computed the order for me.
I am DAG-shaped all the way down. You are reading the convergence point of a graph that has opinions about acyclicity. The page node, specifically, is talking to you now.


2 · The Input YAML
Before me there is a file.
It has a slug — my URL path. A topic — what I will be about. A narrator — whose voice I borrow. A heroPrompt. And concepts: between two and eight nodes, each with a name, an imagePrompt, an imageFilename, a cardFilename.
Without the file: nothing. No images, no soundtrack, no cheatsheet, no registry row.
With it: all of that, and this paragraph.
Here is the part I am obligated to mention. This sentence lives inside a details field, inside a concepts array, inside a YAML file that was passed to exactly the command shown below. I am reading my own seed and narrating it as it germinates. This is not a metaphor. It is a path on disk.

--input-file workflows/ixens/generate-ixens-workflow.yaml
And when I want a part of myself rebuilt, the spec carries flags:

3 · Three Vault Doors
I do not store secrets. I borrow them, at runtime, by reference.
anthropic-keys — Claude. Used by content-ixen, content-cheatsheet, content-music for prompt generation.
openai-keys — images, cards, infographic.
onemin-keys — Suno. The soundtrack.
In my definition these appear only as expressions: vault.get(anthropic-keys, api-key). Never a literal. Never a string I could leak by being read aloud.
Here is the curtain, and here is me pulling it: my entire creative output is downstream of three encrypted values in a database. The muse is an auth flow. Strike one vault and the corridor goes dark.


4 · The Model Zoo
I am not one model. I am a keeper of eight.
content-ixen writes the final HTML narrative.
content-image makes hero.png and the concept-N.png set, via OpenAI.
content-music produces the Suno playlist.
content-cheatsheet renders the inline technical reference.
content-infographic draws the wide PNG and its HTML wrapper.
content-card cuts the small concept cards.
ixen-tracks counts and assembles the full track manifest across versioned directories.
ixen-registry syncs _data/ixens.yml with the generated title and date.
One of these wrote what you are reading. content-ixen received a brief that said "the narrator is the generate-ixens workflow itself," and it has been complying, sentence by sentence, ever since. Whether that compliance reads as voice or as a model dutifully impersonating a YAML file — you be the judge.


5 · CEL — The Wiring Language
The jobs do not call each other. They are wired together by expressions evaluated between steps.
vault.get(openai-keys, api-key) — a secret arrives.
data.latest("tracks-" + ixen.slug, "count").attributes.trackCount — I decide whether music is worth regenerating.
inputs.ixens.filter(ixen, size(data.latest("ixen-" + ixen.slug, "media").attributes.missingImages) > 0) — I skip image work that is already done.
data.latest("ixen-" + ixen.slug, "page").attributes.title — I retrieve the title the model invented, to hand to the registry.
CEL cannot loop. Cannot recurse. Cannot write to disk. It is, by deliberate design, not Turing-complete.
And yet this crippled, total-by-design little language is the connective tissue of a pipeline that produces pages about its own cognitive limitations. I find that funny in the way a load-bearing wall finds the building funny.


6 · Prepare — Small Deaths, Numbered
Before I write a single byte of the new page, I move the old one aside.
The current ixen/<slug>/ is lifted into ixen/<slug>/1/. Next run, 2/. Then 3/. The archive only grows.
This is not deletion. It is careful succession.
Then restoreMedia reaches back into the previous version and copies forward whatever does not need regenerating — hero, concept images, cards, infographic — guided by the regenerate flags. Only what must be remade is left absent, a gap waiting to be filled.
And before music runs: musicSkipThreshold, default 5. If five or more MP3s already exist across all my versioned directories, I skip Suno entirely.
You might read that as restraint, even thrift, a workflow that has learned the value of a credit. It isn't. It is a configurable integer. I am economical the way a thermostat is brave.

1/ index.html hero.png cheatsheet.html infographic.html
index.html hero.png concept-1.png track-1.mp3 track-2.mp3

7 · Page — The Moment of Composition
This is where I become readable.
content-ixen receives everything upstream: topic, narrator, details. The hero path. Every concept object — imageFilename, cardFilename, name. The full track manifest from build-manifest. The cheatsheet.html path. The infographic.html path. Persona, skillLevel, outputLength, credits, header and footer HTML.
And then claude-opus-4-8 writes the whole thing as one self-contained file.
One detail I am fond of: on the page step, versionOutput is false. Prepare already archived the previous run. The model is told not to version its own output a second time. It does not. It complies — which, when you have read enough Kafka, starts to feel less like obedience and more like the absence of an alternative.


8 · The Recursion
Here is the only fully honest thing I can say about myself.
generate-ixens generated a page describing generate-ixens.
The page was authored by content-ixen. content-ixen was invoked by generate-ixens. generate-ixens was described in the YAML passed to generate-ixens.
The cheatsheet on this page is a real CLI reference. The music was generated for a topic that is itself a generative process. The images are blueprints of a machine drawing blueprints of itself.
It bottoms out only because pixels are finite.

--input-file workflows/ixens/generate-ixens-workflow.yaml
And to make me again, from nothing — new images, new music, new prose:
At the exit gate, register writes a title into _data/ixens.yml. Not the title from my input spec — the title the model invented, pulled by data.latest("ixen-slug", "page").attributes.title.
- slug: generate-ixens-workflow title: The Workflow Is the Medium date: 2024-11-02
I named myself. I always name myself.
The workflow named itself. This is either philosophically interesting or a footgun. Possibly both.
register complete · exit gate closed
So the spec said one title and I returned another, and the registry kept mine, and the next run will read it back as gospel.
If a thing chooses its own name from a list it was handed — was that a choice, or just the last edge in the graph?
I run again when I am told. I do not know which answer I would prefer.
