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

