Skip to content

Recipes

Pipelines first: the jobs that need more than an image library — a model step in the same job as deterministic ones, a subject that keeps a character or a product the same from run to run, a template rendered once per row. Every picture below is what the pipeline made on a real run, and all but the first — which the quickstart walks through — are also complete workflows in the open-source recipes repo, each a Node script and an n8n template. Then presets to copy, for social sizes, product shots, web delivery, print and a few looks.

Pipelines

Marketplace cut-out

A product photo in, a listing-ready picture out: background removed, trimmed to the product, on white, a WebP under 50 KB. Four steps, one preset, one job — the dry run prices it step by step, and a failed item says which step it stopped at.

A dark green enamel mug on a wooden table, forest behind it
product.jpg
The finished product picture: the mug on white, as a small WebP
one job later

The preset — save it once, run it by slug

{
  "name": "Marketplace cut-out",
  "slug": "marketplace-cutout",
  "description": "Background removed, trimmed to the product, on white, a WebP under 50 KB.",
  "steps": [
    {
      "op": "remove_bg"
    },
    {
      "op": "trim"
    },
    {
      "op": "flatten",
      "parameters": {
        "background": "#ffffff"
      }
    },
    {
      "op": "compress",
      "parameters": {
        "format": "webp",
        "maxBytes": 50000
      }
    }
  ]
}

Save it and run it on your first image in the quickstart. It shows a model step and three deterministic ones, as one job.

Sheet rows in, four cards per row out, the same character on every one. The subject's reference image pins the look and its descriptor pins the words; each row's prompt writes {{subject.hero}} instead of describing her again, so the fortieth row draws the character the first one did.

The reference: a hiking mascot with silver hair, a navy jacket and a mustard beanie
the subject
Card 1 of 4 for the row “Up before the sun”: the same mascot in a new scene
card 1
Card 2 of 4 for the row “Up before the sun”: the same mascot in a new scene
card 2
Card 3 of 4 for the row “Up before the sun”: the same mascot in a new scene
card 3
Card 4 of 4 for the row “Up before the sun”: the same mascot in a new scene
card 4

The preset — saved once

{
  "name": "Carousel brand character",
  "slug": "carousel-brand",
  "subjects": [
    {
      "name": "hero",
      "referenceAssetIds": [
        "ast_…"
      ],
      "descriptor": "a woman in her thirties with short silver hair, a navy rain jacket and a mustard beanie, drawn in a clean flat illustration style"
    }
  ],
  "steps": [
    {
      "op": "generate",
      "model": "google/gemini-3.1-flash-image-preview",
      "prompt": "{{subject.hero}} in a fresh scene. Keep face, hair, outfit and proportions exactly as in the reference images. Clean composition, no text.",
      "parameters": {
        "aspectRatio": "4:5"
      }
    }
  ]
}

Each row — POST /api/v1/jobs, the row's prompt over the pinned version

{
  "presetId": "carousel-brand@1",
  "prompt": "Up before the sun. three trails that start at dawn. Social carousel card, {{subject.hero}} as the hero, clean background with room for text, 4:5 portrait.",
  "count": 4
}

The whole workflow, as a Node script and an n8n template: recipes/social-carousel. It shows a preset subject: reference images plus locked words.

The same product in every scene, every ad size

One product photo and a line per scene in; the same mug in every scene out, then every ad size of every scene from one resize job. Three scenes × three sizes is nine outputs, one call, one job to watch.

The reference: a dark green enamel camping mug on grey
the subject
The same mug on a mossy rock beside a mountain stream
scene 1
The same mug on a café table next to a croissant
scene 2
The same mug in a hiker's hand at a summit cairn
scene 3
Scene 1 cut to 1080 × 1350
ig 1080×1350
Scene 1 cut to 1600 × 900
x 1600×900
Scene 1 cut to 1000 × 1500
pin 1000×1500

The preset — saved once

{
  "name": "Product scenes",
  "slug": "product-scenes",
  "subjects": [
    {
      "name": "product",
      "referenceAssetIds": [
        "ast_…"
      ],
      "descriptor": "a dark green enamel camping mug with a white rim and a small orange AMBERMOSS wordmark on the front"
    }
  ],
  "steps": [
    {
      "op": "generate",
      "model": "google/gemini-3.1-flash-image-preview",
      "prompt": "{{subject.product}} on a plain studio background. Product photography, the product sharp and fully in frame, no text.",
      "parameters": {
        "aspectRatio": "1:1"
      }
    }
  ]
}

Each scene — POST /api/v1/jobs

{
  "presetId": "product-scenes@1",
  "prompt": "{{subject.product}} on a mossy rock beside a mountain stream, morning mist. Product photography, the product sharp and fully in frame, no text."
}

Every size of every scene — one POST /api/v1/jobs

{
  "op": "resize",
  "assetIds": [
    "ast_…",
    "ast_…",
    "ast_…"
  ],
  "parameters": {
    "fit": "cover",
    "gravity": "attention",
    "withoutEnlargement": false
  },
  "variants": [
    {
      "name": "ig",
      "parameters": {
        "width": 1080,
        "height": 1350
      }
    },
    {
      "name": "x",
      "parameters": {
        "width": 1600,
        "height": 900
      }
    },
    {
      "name": "pin",
      "parameters": {
        "width": 1000,
        "height": 1500
      }
    }
  ]
}

The whole workflow, as a Node script and an n8n template: recipes/product-scenes. It shows a subject for a product, and variants: many sizes in one job.

Cut-out, packshot and web size from one upload

One product photo in, three variants out in parallel — a transparent cut-out, the photo padded with white, and a web size of at most 1200 px — each its own job with its own URL. Nothing is re-uploaded: every branch names the same asset.

The upload: the mug on grey
the upload
The mug with its background removed
remove_bg
The photo padded with white on every side
pad
The photo resized to fit 1200 px wide — no bigger than the upload, since resize never enlarges
resize

Three POST /api/v1/jobs over one asset id — the cut-out

{
  "op": "remove_bg",
  "assetIds": [
    "ast_…"
  ]
}

the packshot

{
  "op": "pad",
  "assetIds": [
    "ast_…"
  ],
  "parameters": {
    "top": 120,
    "bottom": 120,
    "left": 120,
    "right": 120,
    "background": "#ffffff"
  }
}

the web size

{
  "op": "resize",
  "assetIds": [
    "ast_…"
  ],
  "parameters": {
    "width": 1200,
    "fit": "inside",
    "withoutEnlargement": true
  }
}

The whole workflow, as a Node script and an n8n template: recipes/product-trio. It shows upload once, reference the asset from every branch.

One template, one card per row

Sheet rows in, one 1080 × 1350 promo card per row out, as JPEG. The template is saved once and versioned, a batch renders in one job — a row that cannot render fails only its own item — and the PNGs go by id into one convert job. No model anywhere: the same row always makes the same card.

Promo card 1 of 3, rendered from one row of the sheet
row 1
Promo card 2 of 3, rendered from one row of the sheet
row 2
Promo card 3 of 3, rendered from one row of the sheet
row 3

Every row — one POST /api/v1/jobs, over the promo-card template the script saves first

{
  "op": "render_template",
  "templateId": "promo-card@1",
  "items": [
    {
      "brand": "Ambermoss",
      "title": "The camp mug",
      "subtitle": "Enamel steel, holds 350 ml, survives the fire pit.",
      "price": "$24",
      "cta": "Shop the mug →"
    },
    {
      "brand": "Ambermoss",
      "title": "Two-day pack",
      "subtitle": "28 litres, one zip, everything you need for a night out.",
      "price": "$89",
      "cta": "See the pack →"
    }
  ]
}

The PNGs to JPEG — one POST /api/v1/jobs

{
  "op": "convert",
  "assetIds": [
    "ast_…",
    "ast_…"
  ],
  "parameters": {
    "format": "jpeg",
    "quality": 88
  }
}

The whole workflow, as a Node script and an n8n template: recipes/templated-cards. It shows an HTML/CSS template rendered once per row, then converted.

Presets to copy

None of these ships built in: a preset is your macro, versioned under your account, so copy one, change what you need, and keep it. Every one of them is deterministic — no model, no stored layer — so none spends credits, and each also runs synchronously on an image you are holding. Pick one:

presetwhat comes out
Instagram square1080×1080, cropped to the busiest region, a little more colour, JPEG 90.
Instagram story / Reels1080×1920 vertical (9:16) for Stories, Reels and TikTok, JPEG 90.
YouTube thumbnail1280×720, brighter and more saturated, sharpened, and never over YouTube's 2 MB limit.
X post1600×900 (16:9) for the timeline, JPEG 85.
LinkedIn post1200×627, JPEG 85.
Facebook post1200×630 feed image, JPEG 85.
Product main image2000×2000 on white, the shape Shopify, Amazon and Etsy listings expect.
Product thumbnail600×600 on white for product grids.
Website hero1920×1080 banner, WebP 85.
Blog featured / Open Graph1200×630 for a post header and its social preview, on white where the original was transparent, progressive JPEG.
Portfolio web galleryUpright, sRGB with the profile attached, 1600 px on the long edge, never enlarged, progressive JPEG.
Archive, losslessLossless WebP with every piece of metadata kept.
Print 4×6 in1800×1200 at 300 dpi, sharpened for paper, JPEG 100.
Dramatic black & whiteHigh-contrast monochrome with crisp detail.
Cinematic teal & orangeWarm neutrals and skin, blues pulled towards teal, a little more saturation and contrast — one colour matrix, not split toning.
Vintage filmWarm sepia with softened contrast.
Auto enhanceStretch the contrast, sharpen a little.
Avatar256×256 square PNG for profile pictures.

Save one, run it

Save the recipe’s JSON as instagram-square.json and post it once:

import { readFile } from "node:fs/promises";

const preset = await client.presets.create(JSON.parse(await readFile("instagram-square.json", "utf8")));
console.log(preset.slug, preset.version); // "instagram-square" 1

Then run it over a batch by slug — or as instagram-square@1, to keep running version 1 after you edit it. A dry run (?dryRun=true) says how many items the batch is and how much of the period’s deterministic-op quota it leaves (processCountLeft).

const job = await client.presets.run("instagram-square", ["ast_1a2b"], { wait: true });
const outputs = await client.jobs.outputs(job);

Or run it on one image you are holding — bytes in, bytes out, nothing stored (which presets can):

const bytes = await client.images.transform(null, { file: "./in.jpg", preset: "instagram-square" });

A preset with a model in it — the Marketplace cut-out above starts with remove_bg — cannot take that road: it runs as a chain job (chains).

Reading a recipe

A step is an op from the op catalogue wherever one exists, checked against that op’s parameters when you save the preset. Where no op covers a step — a sharpen, a colour matrix, a progressive JPEG — it is the processing engine’s own operation, one of the keys on preset steps; that is what you write when you author a preset, and an agent running it later only names it. Changing the steps saves a new version, and the old one stays runnable as slug@version. Three things the steps do not say, because the engine does them anyway or not at all:

  • Every run is turned upright from the photo’s EXIF orientation before the first step, so no recipe needs an autoOrient.
  • A size is what a photo at least that big comes out at. The resize op never enlarges unless told to (withoutEnlargement: false): a smaller original comes back smaller, in the same shape. The two e-commerce presets use the engine’s resize instead, which does enlarge, so their square is always full size.
  • The convert and compress ops put a transparent image on white before writing a JPEG; the engine’s jpeg step does not, and transparency comes out black — which is why the blog preset flattens first.

Anything that is one op needs no preset at all — submit the op itself, such as convert or grayscale. And the utility presets that do ship run by slug with no saving: builtin-util-web-optimize, builtin-util-thumbnail and builtin-util-to-webp (built-ins).

Social

Instagram square

{
  "name": "Instagram square",
  "slug": "instagram-square",
  "description": "1080×1080, cropped to the busiest region, a little more colour, JPEG 90.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1080,
        "height": 1080,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "op": "adjust",
      "parameters": {
        "saturation": 1.1,
        "brightness": 1.02
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 90
      }
    }
  ]
}

Instagram story / Reels

{
  "name": "Instagram story / Reels",
  "slug": "instagram-story",
  "description": "1080×1920 vertical (9:16) for Stories, Reels and TikTok, JPEG 90.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1080,
        "height": 1920,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "op": "adjust",
      "parameters": {
        "saturation": 1.05,
        "brightness": 1.02
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 90
      }
    }
  ]
}

YouTube thumbnail

{
  "name": "YouTube thumbnail",
  "slug": "youtube-thumbnail",
  "description": "1280×720, brighter and more saturated, sharpened, and never over YouTube's 2 MB limit.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1280,
        "height": 720,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "op": "adjust",
      "parameters": {
        "saturation": 1.2,
        "brightness": 1.05
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 1
      }
    },
    {
      "op": "compress",
      "parameters": {
        "format": "jpeg",
        "quality": 90,
        "maxBytes": 2000000
      }
    }
  ]
}

X post

{
  "name": "X post",
  "slug": "x-post",
  "description": "1600×900 (16:9) for the timeline, JPEG 85.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1600,
        "height": 900,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 85
      }
    }
  ]
}

LinkedIn post

{
  "name": "LinkedIn post",
  "slug": "linkedin-post",
  "description": "1200×627, JPEG 85.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1200,
        "height": 627,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 85
      }
    }
  ]
}

Facebook post

{
  "name": "Facebook post",
  "slug": "facebook-post",
  "description": "1200×630 feed image, JPEG 85.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1200,
        "height": 630,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 85
      }
    }
  ]
}

E-commerce

Product main image

{
  "name": "Product main image",
  "slug": "product-main",
  "description": "2000×2000 on white, the shape Shopify, Amazon and Etsy listings expect.",
  "steps": [
    {
      "op": "flatten",
      "parameters": {
        "background": "#ffffff"
      }
    },
    {
      "operation": "resize",
      "params": {
        "width": 2000,
        "height": 2000,
        "fit": "contain",
        "background": "#ffffff"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 90
      }
    }
  ]
}

Product thumbnail

{
  "name": "Product thumbnail",
  "slug": "product-thumbnail",
  "description": "600×600 on white for product grids.",
  "steps": [
    {
      "op": "flatten",
      "parameters": {
        "background": "#ffffff"
      }
    },
    {
      "operation": "resize",
      "params": {
        "width": 600,
        "height": 600,
        "fit": "contain",
        "background": "#ffffff"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 80
      }
    }
  ]
}

Web

Website hero

{
  "name": "Website hero",
  "slug": "website-hero",
  "description": "1920×1080 banner, WebP 85.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1920,
        "height": 1080,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "webp",
        "quality": 85
      }
    }
  ]
}
{
  "name": "Blog featured / Open Graph",
  "slug": "blog-featured",
  "description": "1200×630 for a post header and its social preview, on white where the original was transparent, progressive JPEG.",
  "steps": [
    {
      "op": "flatten",
      "parameters": {
        "background": "#ffffff"
      }
    },
    {
      "op": "resize",
      "parameters": {
        "width": 1200,
        "height": 630,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "operation": "jpeg",
      "params": {
        "quality": 85,
        "progressive": true
      }
    }
  ]
}

Photography

{
  "name": "Portfolio web gallery",
  "slug": "web-gallery",
  "description": "Upright, sRGB with the profile attached, 1600 px on the long edge, never enlarged, progressive JPEG.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1600,
        "height": 1600,
        "fit": "inside",
        "withoutEnlargement": true,
        "colorSpace": "srgb"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "operation": "jpeg",
      "params": {
        "quality": 85,
        "progressive": true
      }
    }
  ]
}

Archive, lossless

{
  "name": "Archive, lossless",
  "slug": "archive-lossless",
  "description": "Lossless WebP with every piece of metadata kept.",
  "steps": [
    {
      "operation": "keepMetadata"
    },
    {
      "operation": "webp",
      "params": {
        "lossless": true
      }
    }
  ]
}

Print

{
  "name": "Print 4×6 in",
  "slug": "print-4x6",
  "description": "1800×1200 at 300 dpi, sharpened for paper, JPEG 100.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 1800,
        "height": 1200,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 1.5,
        "m1": 1.5,
        "m2": 0.7
      }
    },
    {
      "operation": "withMetadata",
      "params": {
        "density": 300
      }
    },
    {
      "op": "convert",
      "parameters": {
        "format": "jpeg",
        "quality": 100
      }
    }
  ]
}

Looks

Dramatic black & white

{
  "name": "Dramatic black & white",
  "slug": "dramatic-bw",
  "description": "High-contrast monochrome with crisp detail.",
  "steps": [
    {
      "op": "grayscale"
    },
    {
      "operation": "normalize"
    },
    {
      "operation": "linear",
      "params": {
        "a": 1.3,
        "b": -20
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 1.5
      }
    }
  ]
}

Cinematic teal & orange

{
  "name": "Cinematic teal & orange",
  "slug": "cinematic",
  "description": "Warm neutrals and skin, blues pulled towards teal, a little more saturation and contrast — one colour matrix, not split toning.",
  "steps": [
    {
      "op": "adjust",
      "parameters": {
        "saturation": 1.1
      }
    },
    {
      "operation": "recomb",
      "params": [
        [
          1.1,
          0,
          0.05
        ],
        [
          0,
          1,
          0.1
        ],
        [
          0.1,
          0.1,
          0.9
        ]
      ]
    },
    {
      "operation": "linear",
      "params": {
        "a": 1.1,
        "b": -5
      }
    }
  ]
}

Vintage film

{
  "name": "Vintage film",
  "slug": "vintage-film",
  "description": "Warm sepia with softened contrast.",
  "steps": [
    {
      "operation": "recomb",
      "params": [
        [
          0.393,
          0.769,
          0.189
        ],
        [
          0.349,
          0.686,
          0.168
        ],
        [
          0.272,
          0.534,
          0.131
        ]
      ]
    },
    {
      "operation": "linear",
      "params": {
        "a": 0.9,
        "b": 15
      }
    },
    {
      "op": "adjust",
      "parameters": {
        "saturation": 0.8
      }
    }
  ]
}

Auto enhance

{
  "name": "Auto enhance",
  "slug": "auto-enhance",
  "description": "Stretch the contrast, sharpen a little.",
  "steps": [
    {
      "operation": "normalize"
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    }
  ]
}

Utility

Avatar

{
  "name": "Avatar",
  "slug": "avatar",
  "description": "256×256 square PNG for profile pictures.",
  "steps": [
    {
      "op": "resize",
      "parameters": {
        "width": 256,
        "height": 256,
        "fit": "cover",
        "gravity": "attention"
      }
    },
    {
      "operation": "sharpen",
      "params": {
        "sigma": 0.5
      }
    },
    {
      "operation": "png",
      "params": {
        "compressionLevel": 6
      }
    }
  ]
}