Skip to main content

Bulk variants

Problem: The user wants several versions of a design — personalized per persona, per customer, per channel, per brand. Bulk is the workflow that breaks the most “make one design” assumptions in the rest of this skill, so read this in full before fanning out. There are three valid shapes. Pick based on what the user already has and what they want: Carousel (Pattern C) page count clamps to the caller’s plan tier, same as slides — see ../references/gotchas.md#format-and-dimensions. The rest of this recipe is about A and B.

The single biggest gotcha — concurrency caps

start_design_task and remix_design share a per-organization in-flight cap (pooled across every team in the org, not per-team): Fan out N > cap and you’ll get an AgentJobRateLimitError partway through — surfacing as a tool error from start_design_task. The skill’s job is to never let the user see that error. Use a windowed launch: keep at most cap tasks in flight, slot in the next prompt every time one terminates. Get the cap from get_moda_bootstrap — its concurrency_cap field is the exact number for the user’s plan. If you haven’t called get_moda_bootstrap yet, default to 3 (safe everywhere) and bump up after the first batch confirms headroom.

Pattern A — fan out start_design_task (N independent designs)

Best when each variant has fundamentally different content: per-persona ad copy, per-customer pitch decks, per-region landing pages. There is no source canvas to preserve structure from. User: “Create 10 LinkedIn ads for our automation tool — each tailored to a different ICP persona: CFOs, CTOs, heads of ops, …” Agent (after the gate, before fanning out):
Kicking off 10 LinkedIn ads, one per persona. I’ll run them in batches of 3 (concurrency cap) and post each canvas URL as soon as it’s ready — you can open them while the others are still rendering.
Notes on the loop:
  • Never free a slot on absence-from-pool alone. list_tasks(status="running", ...) is a cheap pre-filter, not the correctness check — a task can be missing from it while still “queued” (not yet running) rather than terminal. Only get_task_status(...)["is_terminal"] == true may remove a handle from in_flight and launch its replacement; skipping this check overruns the concurrency window the whole recipe exists to respect.
  • list_tasks(status="running", ...) is one RPC for the whole pool. Beats get_task_status × N per tick.
  • Post each canvas URL when the task is queued, not when it finishes. The user can open it immediately — they’ll see the agent’s progress in the canvas itself.
  • Deliver each result as it terminates. Bulk feels much faster when 1/10 done shows up in 90s than when all 10 land together at 8 min.
  • post_done(persona, s) surfaces s["error"] if the task failed. Don’t let one bad persona block the rest.

What goes in the prompt vs the parameters

  • Per-persona detail belongs in the prompt — rewrite the pain point, metric, and CTA for each. Don’t just template {persona} into a generic sentence.
  • Dimensions and format_category stay constant across the batch. They’re parameters.
  • One brand kit for all unless the user says otherwise. Omit brand_kit_id so each task picks up the team default. Don’t fetch the kit per task — fetch once before the loop.

Pattern B — fan out start_design_task with template_canvas_id (N variants of a source canvas)

Best when the user has an existing canvas they’re happy with and wants variations of that design — same structure, different copy, different brand. The server copies the source, applies the resolved brand kit on the copy, then runs the agent against the copy. The original is never touched. Skill selection is automatic from brand-kit comparison:
  • Same brand kit as the source → content-only remix (preserve design, swap content)
  • Different brand_kit_id → full rebrand (rework colors, fonts, copy, imagery)
  • skip_brand_kit=True → no forced skill; the curator decides
User: “I love this product flyer — make me 5 versions, one for each of these resellers: [list]. Same layout, swap the logo and the regional pricing.”
Why template_canvas_id over start_design_task(canvas_id=source_id, ...):
  • canvas_id edits the original. Five resellers, one canvas — the last one wins. The user loses their template.
  • template_canvas_id duplicates first. Original stays clean; you get N independent copies.
  • Layout / structure carry forward automatically. You only specify what changes in the prompt.
  • Per-variant brand swap is first-class. Pass a different brand_kit_id per call to produce the same design rebranded for each client.
Note: template_canvas_id is mutually exclusive with both canvas_id and conversation_id — passing either combination raises a tool error. See ../references/gotchas.md#conversations-vs-canvases-vs-remixes for the full matrix.

When to use remix_design instead

remix_design(canvas_id=…) without a prompt is a synchronous plain duplicate — useful when the user just wants a copy they’ll edit themselves. With a prompt, it’s an older async path that pre-dates template_canvas_id; prefer template_canvas_id for new bulk flows (cleaner brand-kit handling, automatic skill selection, no wait default asymmetry).

Recovery: a task in the batch fails

get_task_status returns {status: "failed", error: "…"} for that task. Common cases:
  • Rate limit (AgentJobRateLimitError text in error) — your window is too wide. Drop it by one, retry that persona/reseller.
  • Billing (out of credits) — stop the whole batch. Tell the user; the remaining queued items would all fail the same way.
  • Validation — bad prompt or parameters for that one task. Skip it, continue with the rest.
  • Upstream model error — retry that one task once. If it fails again, skip.
Don’t silently retry on failed unless you specifically know the error is transient. Surface the persona/reseller that failed so the user can decide.

Aborting a bulk run mid-flight

If the user changes their mind (“never mind, kill them”) or you spot a systemic issue (every task failing the same way), call cancel_task(task_id) on each in-flight handle. Already-terminal tasks are no-ops.

See also

  • ../references/gotchas.mdwait asymmetry, concurrency caps, format defaults, carousel page-count tier clamp, not_ready retry, and the rest of the silent-fail set