Ultron Workers

Pipeline stages

The marketing worker's pipeline is seven discrete stages, each in its own file under src/carousel/. This page walks every stage with its inputs, outputs, and failure mode. Stages run sequentially; the orchestrator short-circuits to a structured error on the first failure.

Updated today

Overview

At a glance
Orchestrator
cf-workers/marketing-swarm/src/carousel/index.ts
Stage files
retrieve, plan, validate, render-v2, qa, promote (one .ts each)
Typical p50
28 seconds end to end
Hard timeout
Cloudflare Worker max execution time
Idempotency
Each run has a unique id; stages do not depend on global state

Stage 1: Retrieve

Embed the brief, query Vectorize, hydrate exemplars.

cf-workers/marketing-swarm/src/carousel/retrieve.tsts
1// 1. Embed the incoming brief
2const { data: [embedding] } = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
3 text: [brief],
4})
5
6// 2. Query Vectorize for the top-k similar past briefs
7const matches = await env.BRIEFS.query(embedding, {
8 topK: 4,
9 filter: { status: "active" },
10})
11
12// 3. Hydrate exemplars from R2 (spec JSON + slide PNGs)
13const exemplars = await Promise.all(matches.matches.map((m) =>
14 loadExemplar(env.EXEMPLARS, m.id)
15))

The retrieval step gives the planner concrete prior art to reason from. Without it the planner falls back on generic visual ideas; with it the planner picks up the style and the slide rhythm from previous carousels that scored well. The filter on status: "active" excludes runs that were retired without being promoted.

Stage 2: Plan

Gemma emits a CarouselSpec given the brief, the exemplars, and the registered component index.

cf-workers/marketing-swarm/src/carousel/plan.tsts
1const result = await env.AI.run("@cf/google/gemma-4-26b-a4b-it", {
2 prompt: `${DSL_SUMMARY}\n\nBRIEF:\n${brief}\n\nEXEMPLARS:\n${exemplarSummary}\n\nCOMPONENT INDEX:\n${componentSummary}\n\nReturn a CarouselSpec in JSON.`,
3 max_tokens: 4096,
4})
5const spec = JSON.parse(extractJson(result.response))
Note
The DSL summary enumerates every constraint (freeform only, position grid, character budgets, required backdrop). It is the only thing standing between the planner and chaos; small edits to it can move slide quality measurably in either direction.

Stage 3: Validate

Structural and budget rules. Reject early.

cf-workers/marketing-swarm/src/carousel/validate.tsts
1function validate(spec: CarouselSpec): ValidationIssue[] {
2 const issues: ValidationIssue[] = []
3 if (spec.slides.length < 3 || spec.slides.length > 12) {
4 issues.push({ path: "slides", message: "Slide count must be 3-12" })
5 }
6 for (const [i, slide] of spec.slides.entries()) {
7 if (slide.kind !== "freeform") {
8 issues.push({ path: `slides[${i}].kind`, message: "FORBIDDEN - must be freeform" })
9 }
10 for (const [j, el] of slide.elements.entries()) {
11 if (el.type === "text" && el.value.length > inferBudget(el)) {
12 issues.push({ path: `slides[${i}].elements[${j}]`, message: "Over budget" })
13 }
14 }
15 }
16 return issues
17}

Stage 4: Render

Satori plus Resvg WASM, all in the worker.

cf-workers/marketing-swarm/src/carousel/render-v2/index.tsts
1for (const slide of spec.slides) {
2 const jsx = compileSlide(slide, theme, libraryRegistry)
3 const svg = await satori(jsx, { width: 1080, height: canvasHeight, fonts })
4 const png = new Resvg(svg, { fitTo: { mode: "width", value: 1080 } }).render().asPng()
5 pngs.push({ index: slide.index, bytes: png })
6}

Compile turns the DSL into JSX. Satori turns JSX into SVG. Resvg WASM rasterises SVG into PNG. The entire chain runs inside the worker process; no separate cloud-runtime renderer is involved. Fonts are loaded from R2 at worker boot and kept warm in the module-level cache.

Stage 5: QA

LLaVA scores every slide on the same rubric.

cf-workers/marketing-swarm/src/carousel/qa.tsts
1for (const slide of pngs) {
2 const verdict = await env.AI.run("@cf/llava-1.5-7b", {
3 image: slide.bytes,
4 prompt: QA_RUBRIC, // legibility, hierarchy, alignment, on-brand
5 max_tokens: 256,
6 })
7 scores.push(parseScore(verdict.description))
8}
9const average = scores.reduce((a, b) => a + b, 0) / scores.length
Tip
The rubric is short and operational. The model returns a numeric score plus a one-line critique per slide. Critiques are kept on the run row so a human can later spot patterns across many runs.

Stage 6: Promote

If the average score crosses the threshold, the run becomes an exemplar for next time.

cf-workers/marketing-swarm/src/carousel/promote.tsts
1if (averageScore >= 0.85 || userStarred) {
2 await env.DB.prepare(
3 "INSERT INTO carousels (id, brief, spec, score, status) VALUES (?, ?, ?, ?, 'active')"
4 ).bind(runId, brief, JSON.stringify(spec), averageScore).run()
5
6 await env.BRIEFS.upsert([{ id: runId, values: briefEmbedding, metadata: { status: "active" } }])
7
8 for (const slide of pngs) {
9 await env.EXEMPLARS.put(`carousels/${runId}/slide-${slide.index}.png`, slide.bytes)
10 }
11}

Stage 7: Respond

The HTTP response carries the run id, the score, and signed URLs for every slide.

cf-workers/marketing-swarm/src/carousel/index.tsts
1return Response.json({
2 id: runId,
3 score: averageScore,
4 slides: pngs.map((p) => ({
5 index: p.index,
6 url: signedR2Url(env.EXEMPLARS, `carousels/${runId}/slide-${p.index}.png`),
7 })),
8 spec,
9 layers_applied: stagesRan,
10})

Error handling

Every stage can fail. The orchestrator catches structured errors and surfaces them.

StageFailureBehaviour
RetrieveVectorize empty or rate-limitedContinue with no exemplars; planner gets a degraded prompt
PlanModel returns unparseable JSONRetry once; if still bad, fail with 502
ValidateSpec violates the DSLFail with 422 and the validation issues array
RenderSatori or Resvg throws on a slideFail the whole run with 500 and the slide index
QALLaVA times out on a slideUse 0.5 as the fallback score for that slide
PromoteD1 or R2 write failsLog but do not fail the response; the user still gets the deck

File map

cf-workers/marketing-swarm/src/carousel
index.tsorchestrator: run the seven stages in order
retrieve.ts
plan.ts
validate.ts
render-v2
index.ts
qa.ts
promote.ts