RivermarkRivermark
Guides

Fully autonomous pipelines

No human in the loop — generate and schedule content automatically when something happens.

Everything so far had a human somewhere in the loop — picking a template, approving a design. But the API is deterministic and self-reviewing enough to run entirely on its own: a trigger fires, designs are generated, and the output goes straight to where it needs to be. This guide builds the canonical example: when a new product is uploaded to a Shopify store, automatically generate a set of ad ideas and schedule them.

The same shape works for other triggers — a new blog post, a calendar event, an inventory change, a weekly digest — anything your backend can turn into a webhook or a cron job.

The loop

failures Triggernew product uploaded Gather referencesproduct data · photos · brand kit GeneratePOST /design — auto_review on Download renders Publish / schedule Review queuea human, later, optionally

There is no step where a person looks at the output before it ships. That's the point — and it changes a few defaults.

1. The trigger

Listen for new products from Shopify (or whatever your source is). The webhook payload already contains everything you need — title, description, price, photos — so you don't need to fetch anything: turn the structured data into references directly.

TypeScript — webhook handler (framework of your choice)
export async function POST(req: Request) {
  // verify the webhook, then pull the product out of the payload
  const product = await verifyAndParseShopify(req);

  await generateAdsForProduct(product);
  return new Response("ok");
}

2. Gather references and generate

The webhook gave you structured product data and photo URLs. Turn the data into a text reference, upload the photos as file references, and add the store's brand kit (extracted once, when the store onboarded — see brand extraction). The agent then has everything it needs without fetching a thing.

Ask for a few distinct ad ideas as named outputs, and let auto-review do its job — in an autonomous pipeline it's your only quality gate, so it stays on.

generate-ads.ts
type Reference =
  | { type: "text"; name: string; text: string }
  | { type: "file" | "design"; id: string };

async function generateAdsForProduct(product: Product) {
  const auth = { Authorization: `Bearer ${process.env.RIVERMARK_API_KEY}` };

  // 1. Stringify the structured product data from the webhook
  const references: Reference[] = [
    {
      type: "text",
      name: "product.json",
      text: JSON.stringify(
        {
          title: product.title,
          description: product.description,
          price: product.price,
          tags: product.tags,
        },
        null,
        2,
      ),
    },
  ];

  // 2. Upload the product photos as file references
  for (const image of product.images) {
    const blob = await (await fetch(image)).blob();
    const form = new FormData();
    form.append("file", blob, image.split("/").pop());
    const uploaded = await fetch("https://api.rivermark.ai/upload", {
      method: "POST",
      headers: auth,
      body: form,
    }).then((r) => r.json());
    references.push({ type: "file", id: uploaded.id });
  }

  // 3. The store's brand kit, extracted once at onboarding
  references.push({ type: "design", id: await getStoreBrandkitId(product.id) });

  const design = await fetch("https://api.rivermark.ai/design", {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({
      task: [
        "Create 3 ad creative ideas for the product described in product.json.",
        "Use the provided product photos where they fit.",
        "Each ad should highlight a different selling point and work as a paid social ad.",
        "Use the provided brand kit.",
      ].join(" "),
      references,
      creativity: 2, // ideas, not mechanical fills
      output: [
        { id: "ad_1", size: "1080x1350" },
        { id: "ad_2", size: "1080x1350" },
        { id: "ad_3", size: "1080x1350" },
      ],
    }),
  }).then((r) => r.json());

  // download immediately — rendered URLs are ephemeral
  const files: Record<string, Buffer> = {};
  for (const frame of design.output) {
    files[frame.id] = Buffer.from(await (await fetch(frame.url)).arrayBuffer());
  }

  return { design, files };
}

3. Schedule or publish

Push the downloaded renders to whatever runs your ads or socials. If your platform supports drafts, schedule as drafts — you get autonomy now and a human can still glance at the queue when they feel like it.

TypeScript — scheduling (pseudo-API)
const { design, files } = await generateAdsForProduct(product);

for (const [id, image] of Object.entries(files)) {
  await adsPlatform.createScheduledAd({
    campaign: `product-${product.id}-launch`,
    creative: image,
    designId: design.design_id, // keep it: future edits reference this
    startAt: nextMonday9am(),
  });
}

Running unattended, responsibly

Keep auto-review on. In an interactive editor it's a quality filter; in an autonomous pipeline it's the only reviewer. Turning it off to save cost is how off-brand or broken creative ends up in front of customers.

Pick creativity deliberately. 2 for ideation (the ads themselves), 0 for mechanical derivatives of an approved ad (resizes, copy swaps). See Creativity.

Make work idempotent. Webhooks fire more than once. Key on something stable — the product ID — and check whether you've already generated designs for it before firing again. Design IDs are your audit trail: store them with the campaign.

Bound concurrency and handle failures. Ten products uploaded at once is ten independent generations — run them with bounded concurrency (4–8), and route failures to a review queue instead of retrying blind. A failed generation is cheap to re-run by hand; a half-scheduled campaign isn't.

Watch costs per unit of work. Every response carries cost, so tag each generation with the product/campaign it belongs to and track spend per unit — see Costs. You'll know exactly what "ads per new product" costs you before you scale it.

Validate the trigger's data. The agent can only work with what it's given. If a product arrives with no photos or an empty description, route it to the review queue instead of generating — it will produce better results once the data is complete.

Where to go from here

  • Adaptive creative: when an ad underperforms, continue_from its design ID and ask for a sharper hook — then schedule the variant the same way.
  • Full-funnel sets: one request can emit the ad, the story version, and a carousel — declare them as named outputs.
  • Batch, not event-driven? See batch content from a plan for the same fills driven by scheduled or agent-triggered jobs.

On this page