Guide · workers

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

  1. Plan

    The worker runs plan-clipart-pack with 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

the publishing channelhttp
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_..."
}
The secret is shown once

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.

the crewhttp
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.

one workerhttp
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"
      ]
    }
  }
}
FieldWhy it matters
allowedTemplateIdsEnforced 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.
styleWhitelistBoth the planner’s menu and the enforcement clamp. Different lists per worker is what stops a catalog looking uniform.
countItems 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.
budgetUsdHard cap per content type per shift, enforced against the recorded cost ledger.
itemFields / requiredFieldsWhich planner fields become each child’s intake, and which are mandatory — an item missing a required field is dropped rather than run.
messaging.requirementsFacts 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.

wake it dailyhttp
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/run

Step 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.

what your site receiveshttp
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

four checkshttp
# 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.

SymptomCause and fix
Shift healthy, nothing publishedjob.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 packThe content type’s publishPolicy.mode is hitl. Individual artifacts and whole packs ship through different mechanisms.
Pack staged, cover blamedThe 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 declinedThe 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 modelA template is stale in that environment. Templates are versioned data, not code — re-run your template sync after any template change.
Everything freezes mid-shiftThe 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.
Size budget caps from a real shift

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.