Publish packs with a worker team
An unattended production line: a crew of workers that each own a subject area, plan a themed pack every day, generate every asset, compose a cover, and publish the finished pack to your own site. This is the setup behind clip.art/packs, written so you can rebuild it for yourself or a client.
- Primitives
- Outlet → Team → Workers → Schedule
- Templates
- plan-clipart-pack · generate-clip-art-diffmatte · generate-pack-cover
- Cadence
- One pack per worker per day
- Cost
- ~$0.015 per asset · ~$0.38 per 25-item pack
What one shift does
- Plan
The worker runs
plan-clipart-packwith its specialty, its assigned work, and a snapshot of the recent catalog. Out comes a plan artifact: a style recipe and a manifest of items, each with a fully compiled prompt. Nothing is generated yet — this is the auditable record of what the worker knew and chose. - Generate
Each manifest item becomes one child run of the target template, all under a single Generation Order. Failures are per item; the order tracks succeeded, failed, held, and skipped counts.
- Mockups
Product mockups are composed from assets spread across the pack, so the listing shows the goods in use rather than a grid of transparent files.
- Cover
The pack’s hero assets and the plan’s cover concept feed
generate-pack-cover. The cover is part of the pack’s identity — reviewers and buyers meet the pack the same way. - Publish
On a clean pack whose content type sets
publishPolicy: { "mode": "auto" }, the assembled payload is signed and POSTed to your ingest URL. Anything less than clean is staged for review instead, with the reason recorded. - Report
A first-person report lands in your Inbox with a manifest attached: every image and its URL, the pack’s published address, and the reason for anything that did not ship.
Step 1 — Create the outlet
An outlet is a channel defined by a URL: a site plus a section. It carries the ingest endpoint and the signing secret, and records delivery health so you can see whether your site actually accepted the last push.
POST /v1/outlets
{
"name": "clip.art packs",
"slug": "clip-art-packs",
"siteUrl": "https://clip.art",
"sectionPath": "/packs",
"acceptedKinds": [],
"revalidateUrl": "https://clip.art/api/ingest/packs"
}
// 201 — revalidateSecret is revealed EXACTLY ONCE. Store it now.
{
"id": "outlet-553694cb",
"slug": "clip-art-packs",
"revalidateSecret": "whsec_..."
}revalidateSecret is returned only on create and rotate. Deliveries are signed with a Standard Webhooks HMAC over the exact request bytes — verify that signature before trusting a payload. See Connect a consumer site for a worked verification example.
Step 2 — Create the team
Designating an outlet on the team makes it a fence: every member’s publishable output ships there, and no member-level setting can override it. That is what you want for a crew whose entire purpose is one destination.
POST /v1/teams
{
"workspaceId": "9a1b6d4c-...",
"name": "ClipArt Pack Team",
"produces": "Themed clip-art packs, shipped to clip.art/packs",
"outletId": "outlet-553694cb"
}
// 201
{ "id": "team-41861124" }Step 3 — Create the workers
Give each worker a narrow subject area. The specialty is not decoration — it is the standing demand signal that steers every plan, so “automotive” and “holidays” build genuinely different catalogs rather than two flavors of one.
POST /v1/workers
{
"workspaceId": "9a1b6d4c-...",
"name": "Axle",
"title": "Automobile clipart producer",
"specialty": "Cars, trucks, motorcycles, garages and everything automotive",
"teamId": "team-41861124",
"allowedTemplateIds": [
"plan-clipart-pack",
"generate-clip-art-diffmatte",
"generate-pack-cover",
"map-clip-art-to-print-on-demand"
],
"job": {
"publishPolicy": "classified",
"failureThreshold": 0.2,
"contentTypes": [
{
"key": "cars-classic",
"planTemplate": "plan-clipart-pack",
"targetTemplate": "generate-clip-art-diffmatte",
"artifactType": "clip-art",
"count": 25,
"budgetUsd": 1.60,
"itemFields": ["prompt", "style"],
"requiredFields": ["prompt"],
"styleWhitelist": ["3d", "clay", "kawaii", "chibi", "sticker", "emoji"],
"intakeBase": { "quality": "low", "aspectRatio": "1:1" },
"planIntakeExtra": {
"audience": "car enthusiasts and auto shops",
"packGoal": "classic and vintage car graphics",
"style": "3d"
},
"publishPolicy": { "mode": "auto" }
}
],
"messaging": {
"cadence": "every-shift",
"requirements": [
"how many images you created, and their categories",
"for each pack: its title and the address it published to",
"any pack that did NOT publish, and why it was staged instead"
]
}
}
}| Field | Why it matters |
|---|---|
allowedTemplateIds | Enforced at run time. A worker cannot execute a template outside this list, whatever its job says. |
job.publishPolicy: "classified" | Without this the worker publishes nothing — the default is "none". The most common reason a correct-looking setup produces nothing on the site. |
contentTypes[].publishPolicy: { mode: "auto" } | A different knob: this is what pushes a finished PACK to your ingest endpoint. "hitl" (the default) stages it for review instead. |
styleWhitelist | Both the planner’s menu and the enforcement clamp. Different lists per worker is what stops a catalog looking uniform. |
count | Items per shift for this content type. A count of 0 skips it entirely — no plan, no order, no cost — which is how you park a category until you want it. |
budgetUsd | Hard cap per content type per shift, enforced against the recorded cost ledger. |
itemFields / requiredFields | Which planner fields become each child’s intake, and which are mandatory — an item missing a required field is dropped rather than run. |
messaging.requirements | Facts the report must contain. Each entry becomes a “must include” instruction the worker cannot omit. |
Step 4 — Put it on a schedule
Five-field cron, evaluated in UTC. Stagger the crew so they do not all wake into the same rate limit.
POST /v1/schedules
{
"workspaceId": "9a1b6d4c-...",
"name": "Axle daily shift",
"cron": "0 15 * * *",
"workerId": "worker-f6b2a87e"
}
// Test without waiting for the cron:
POST /v1/workers/worker-f6b2a87e/runStep 5 — Receive the pack
Your endpoint receives the whole pack in one signed payload. Everything is idempotent on esyOrderId, so a re-push is safe: accept a partial ingest with 202 and Esy will deliver again on the next publish.
POST https://your-site.com/api/ingest/packs
webhook-signature: v1,<base64 HMAC over the exact request bytes>
{
"pack": {
"esyOrderId": "order-5cda28c2",
"planArtifactId": "artifact-28f2a14b",
"title": "25 Vintage Garage Clipart",
"slug": "25-vintage-garage-clipart",
"shortDescription": "...",
"longDescription": "...",
"category": "vehicles",
"additionalCategories": ["retro", "workshop"],
"tags": ["garage", "vintage", "automotive"],
"packFacts": { "subjects": 6, "poses": 5, "props": 6, "scenes": 4 },
"cover": { "url": "https://images.../cover.webp", "alt": "..." },
"media": [ { "url": "https://images.../mockup-mug.webp" } ],
"items": [ { "title": "Weathered pickup", "url": "https://images.../a.webp" } ]
}
}Verifying a run
# Did the shift complete healthy?
GET /v1/workers/{workerId}/shifts?limit=1
# What did each step actually cost?
GET /v1/costs?groupBy=operation&workspaceId={ws}&workflowId=generate-clip-art-diffmatte
# Did anything file into the outlet?
GET /v1/outlets/{outletId}/items
# Is anything waiting on a human?
GET /v1/queue?workspaceId={ws}When nothing appears on your site
Work these in order — real failure modes, most common first.
| Symptom | Cause and fix |
|---|---|
| Shift healthy, nothing published | job.publishPolicy is unset (defaults to none), or the worker has no home outlet. No team outlet and no solo outlet means publishing exits silently. |
| Assets published, but no pack | The content type’s publishPolicy.mode is hitl. Individual artifacts and whole packs ship through different mechanisms. |
| Pack staged, cover blamed | The cover sits in Review because its classifier could not categorize it. The classifier needs a candidate vocabulary — make sure the plan’s items carry categories, or set intakeBase.categories. |
| Auto-publish declined | The order had failed items. Auto-publish requires a clean pack: retry the failed children, then publish again. |
| A step dies on an unknown tool or model | A template is stale in that environment. Templates are versioned data, not code — re-run your template sync after any template change. |
| Everything freezes mid-shift | The API restarted. Shifts live in the process, so a deploy ends them; the reaper requeues the orphaned runs. Check whether a deploy landed at that timestamp. |
Caps are enforced against recorded spend. Read one finished shift with groupBy=operation and set the cap from that breakdown — an estimate that disagrees with the ledger will trip the cap against the wrong number.