The enrichment Workflows

Four Workflows

One per media type, each a durable job like every other in the kit — media with enrichment is the worked example, and the reference has the bindings.

BindingWhat it derives
MEDIA_IMAGE_TO_TEXTAlt text and captions
MEDIA_AUDIO_TRANSCRIBEA transcript
MEDIA_VIDEO_TRANSCRIBEA transcript, from the video’s audio
MEDIA_DOC_EXTRACTText out of a PDF or Word document

Each reads a record, produces derived content with Workers AI, and writes it back through the record store. The model id is always a parameter, defaulted from config, so swapping a model is a config edit rather than a code change.

A wrong answer and no answer are different faults

This is the distinction the whole retry story rests on.

media/enrichment_failed covers everything deterministic: a response shape the schema does not recognize, a conversion the converter refused, a binding with no toMarkdown. A second attempt produces the same result, so it is terminal.

core/upstream_failed is raised only when the AI binding itself rejects — unreachable, overloaded, out of time. That is the one fault here a retry can answer differently.

The binding’s own words go in detail, which the HTTP codec strips. An account id or a quota message is not something a caller asking about their own file should be handed.

The prompts are defaults, not fixtures

Alt text is capped at 80 tokens and asks for one concise sentence. A caption is capped at 512 and asks for a description.

Both prompts are constants you can override. They are short on purpose: an alt-text prompt that invites detail produces alt text that is worse at being alt text.

Video transcription, and the overlap

A video’s audio is fetched as a Cloudflare Stream HLS audio rendition — an init segment plus ordered media segments.

HLS segments are atomic: you can only fetch and decode one in full. So grouping is whole-segment, greedy, up to a target of 30 seconds per group — long enough to give the model coherent context.

Then the part worth knowing about:

Each group backs up 3 seconds into the previous one. A model asked to transcribe a fixed window routinely drops or garbles the words straddling its edges, and a boundary every 30 seconds is a lot of edges. The overlap gives that audio a second look in the next group.

And then the overlap is removed from the text, by matching and stripping the repeated prefix, so the final transcript reads clean. You get the benefit of the second look without reading everything twice.

The whole algorithm is pure — no Cloudflare imports, no I/O, no clock. The Workflow fetches the playlist, hands the segment list to it, and stitches the per-group transcripts back with it. The durable shell stays thin and the interesting part stays unit-testable.

What is injected, and why that matters to you

Byte access is a seam: reading an object’s bytes, reading a document as a blob, fetching a video’s audio. The Workflow Worker wires the real reads over the R2 binding and the Stream manager.

For you, the practical consequence is that enrichment behavior does not change with where your bytes live — the same orchestration runs whether an image came from R2 or from Images. Only the read behind it differs.

Dispatch

Enrichment runs after finalize, on the record that just became stored. A document outside the extractable extensions dispatches nothing, because there is nothing for the converter to read — that is not a failure and does not appear as one.

ESC