In this step you’ll read the existing parent task and see how it fans one prompt out across the models the caller selected. The pattern is small. Once you’ve internalized it, step 5 is a copy-paste with one extra wrapper.
The two tasks
The reference repo defines two tasks:
- The parent task (
generateThumbnails) takes one prompt and a list of model ids. It spawns one subtask per model and awaits all of them. - The subtask (
generateThumbnail) takes one(prompt, model)pair, calls the provider’s image API, composites the title overlay, and uploads the JPEG to MinIO.
Subtasks run on their own instances. The parent task is light. Most of its wall-clock time is spent waiting on subtask results.
The parent task, annotated
const generateThumbnail = task( { name: "generateThumbnail", retry: { maxRetries: 2, waitDurationMs: 5000, backoffScaling: 2 }, }, async (title, model, style, template, font, context, extraPrompt) => { return generateThumbnailImage(title, model, style, template, font, context, extraPrompt); },);
const _generateThumbnails = task( { name: "generateThumbnails" }, async (title, models, style, template, font, context, extraPrompt) => { const results = await Promise.all( models.map((model) => generateThumbnail(title, model, style, template, font, context, extraPrompt), ), ); return { title, style, template, font, results }; },);app = Workflows( default_retry=Retry(max_retries=2, wait_duration_ms=5000, backoff_scaling=2.0), default_timeout=300,)
@app.task(name="generateThumbnail")async def generate_thumbnail(title, model, style, template, font, context="", extra_prompt=""): return _generate_thumbnail(title, model, style, template, font, context, extra_prompt)
@app.task(name="generateThumbnails")async def generate_thumbnails(title, models, style, template, font, context="", extra_prompt=""): subtasks = [ generate_thumbnail(title, model, style, template, font, context, extra_prompt) for model in models ] results = await asyncio.gather(*subtasks) return {"title": title, "style": style, "template": template, "font": font, "results": results}The subtask call is the fan-out point. Each call to generateThumbnail becomes its own Workflow run with its own retry policy. Promise.all and asyncio.gather are the join: the parent waits until every model finishes, then returns one combined result. Retry belongs on the image-generation subtask because provider failures happen per model, not in the lightweight parent.
What the fan-out looks like at runtime
The parent task is a single instance that lives only as long as it takes the slowest subtask to finish. The subtasks are independent. If Gemini errors and retries, the DALL-E subtask doesn’t notice.
What you learned
- The parent task is small. It spawns subtasks and waits
- Each subtask runs on its own instance, with its own retry policy
- Parent wall-clock = the slowest single subtask, not the sum
- A retry on one subtask doesn't slow down the others