n8n node
One community node and one trigger. Hand it the binary an earlier node produced — from Google Drive, an HTTP Request, a form — and get the transformed image straight back, or keep the result as an asset with a stable public URL; or let the trigger start a workflow when a job finishes, from a signed webhook, so nothing polls. Zero runtime dependencies: every request goes through n8n's own HTTP helpers.
Install and credentials
In n8n: Settings → Community Nodes → Install, enter n8n-nodes-imagestep, accept the prompt. A self-hosted n8n can also take it from a shell — in its nodes directory (inside the container, for Docker):
cd ~/.n8n/nodes && npm install n8n-nodes-imagestepRestart n8n and ImageStep and ImageStep Trigger appear in the node picker. Then create an ImageStep API credential: the API Key is a key from /keys, and Base URL stays https://api.imagestep.dev (change it only for a self-hosted ImageStep). The credential's own test reads the key's usage, which only a valid key can: a green test proves both that the base URL reaches ImageStep and that the key is right, and a wrong or revoked key tests red.
The ImageStep node
One node, a Resource and an Operation dropdown. It is marked usable as an AI-agent tool, so an n8n agent can call it too.
Asset
| operation | what it does | fields |
|---|---|---|
| Upload | Upload a binary file from the input itemtakes the item's binary property, stages, PUTs the bytes, finishes — and with Wait Until Ready on, waits for dimensions and metadata. Output: an asset reference (assetId, name, status, mimeType, width, height, size, collection, publicUrl, expiresAt) | Binary Property (default data) · Collection · Retention Days (default 0) · Wait Until Ready (default on) |
| Upload From URL | Have ImageStep fetch public image URLs and create an asset from each — nothing is downloaded into n8nany number of URLs, sent 20 to a request and waited for together. One output item per URL — the reference plus url, or { url, error } for a link that is private, dead or not an image, so you can route those instead of stopping | Image URLs · Collection · Retention Days (default 0) · Wait Until Ready (default on) |
| Get | Fetch one asset by ID (dimensions, metadata, public URL) | Asset ID |
| List | Search assets by collection, keyword, mime type, ingest state or when they were madeeach item carries page, total, hasMore and nextCursor — page from 0, Per Page 100 at most; Cursor reads the page after a nextCursor | Collection · Search · MIME Type · Status (Any · Done · Failed · Processing) · Created After · Created Before · Page (default 0) · Cursor · Per Page (default 100) |
| List Collections | Your collections with how many assets each holds, most recently added to firstone item per collection — collection, count, lastAddedAt | Name Contains · Page (default 0) · Cursor · Per Page (default 100) |
| Publish | Publish assets so each gets a stable public URL on the CDNcomma-separated ids; each publicUrl serves the asset itself at full size, never a thumbnail | Asset IDs |
Operation
Run — one atomic op, synchronously or as a job. The Op dropdown is loaded live from the catalogue and falls back to a built-in list when that call fails. A field shows only when the ones before it make it apply — Wait Seconds only while Wait for Result is on, and so on.
| field | default | meaning |
|---|---|---|
| Op | default remove_bg | loaded live from the catalogue, with a built-in list when that call fails — /docs/ops is the list |
| Input | default binary; Binary File · Asset IDs · None | Binary File (the item's binary property), Asset IDs (comma-separated; one job over all of them), or None (for generate) |
| Binary Property | default data | Name of the binary property holding the image |
| Asset IDs | One asset ID, or several separated by commas — one job over all of them | |
| Prompt | What to draw (generate) or the edit instruction (edit) | |
| Count | default 1 | Images to generate |
| Parameters | default {} | JSON, e.g. {"width": 1200, "fit": "inside"} for resize or {"scaleFactor": 2} for upscale; a wrong value is a 400 invalid_param naming the parameter, before any credit is spent |
| Dry Run | default off | price only: { dryRun: true, request, estimate } with estimatedCredits, sufficientCredit, processCountLeft and assetCountLeft; nothing is created or uploaded. A Binary File is priced as one image (imageCount: 1 in request), since the price never depends on the pixels |
| Store Result | default off | off: one image in — a Binary File or one Asset ID — and the transformed bytes straight back out on the binary property it came in on (data for an Asset ID), with { op, mode: "sync", stored: false, bytes, mimeType } as the item; nothing created, nothing published, nothing polled. On: it runs as a job, so you get an asset id, a permanent URL, progress and webhooks. Which ops may take the off path is the API's answer (syncEndpoint): AI ops always run as a job, and so does anything that is not one image. A job that runs with this off is waited for (up to 180 s) and publishes nothing — its outputs come back as asset ids with no publicUrl; turn it on to set Wait for Result, Wait Seconds, Publish Outputs, Download Outputs |
| Wait for Result | default on | off returns the job handle at once — continue with Job → Wait or the trigger |
| Wait Seconds | default 180 | How long to wait before giving up (the job keeps running) |
| Publish Outputs | default on | Whether to publish result assets so each output has a stable publicUrl on the CDN: the output itself at full size, not a thumbnail |
| Download Outputs | default off | fetches each output into the binary property data, one item per result |
| Options | Model · Collection · Retention Days | Model overrides the op's default; Collection is where the outputs go (default: the input's) |
Output: a job reference — jobId, type, status, totalItems, completedItems, failedItems, creditsCharged, items[] (the first 100; itemsTruncated past that) and, once finished, outputs[] — every output, with publicUrl. The fields are named as the MCP server names them, so an agent and a workflow read the same keys. Each item carries its own trace — status, resultAssetId, errorCode, retryable, provider, model, credits — and, for a chain, step and failedStep: which segment of the preset it is on, and which one it failed on. With Dry Run the output is { dryRun, request, estimate } instead, and a chain's estimate carries steps[] — the price segment by segment.
Preset and Job
| operation | what it does | fields |
|---|---|---|
| Preset → Run | Run a saved preset over assets as a joba saved preset over assets: built-ins first, then yours; Version 0 runs the current one; every result is a new asset. A preset whose steps mix a model with other steps runs as one chain job. A preset always runs as a job, so Store Result decides only what becomes of its outputs: off, the job is waited for (up to 180 s) and nothing is published — the outputs come back as asset ids; on shows Wait for Result, Wait Seconds, Publish Outputs, Download Outputs | Preset · Version (default 0) · Input (default binary; Binary File · Asset IDs · None) · Binary Property (default data) · Asset IDs · Prompt · Count (default 0) · Dry Run (default off) · Store Result (default off) · Wait for Result (default on) · Wait Seconds (default 180) · Publish Outputs (default on) · Download Outputs (default off) · Options (Collection · Retention Days) |
| Job → Get | Read a job's status and, once finished, its outputsa finished job's outputs are collected and, with Publish Outputs, published | Job ID · Publish Outputs (default on) · Download Outputs (default off) |
| Job → Wait | Poll a job until it finishes and return its outputsthe service holds each read open, so nothing polls; then as Get | Job ID · Wait Seconds (default 180) · Publish Outputs (default on) · Download Outputs (default off) |
The ImageStep Trigger
A webhook trigger. On activation it registers an endpoint on your account pointing at the workflow's webhook URL, subscribed to the Events you pick — job.completed and job.failed by default, job.item.completed and job.item.failed opt-in — and deletes it on deactivation. Every delivery is verified before the workflow runs: the ImageStep-Signature HMAC over the raw body, a timestamp inside Signature Tolerance (300 s by default), a constant-time compare. A delivery that fails is answered 401 and never reaches the workflow; a good one is answered 200 at once and emits { id, type, createdAt, attempt, data }. Deduplicate on id if you must be exactly-once — the API retries a delivery that is not acknowledged, for up to 24 hours; what each event's data holds is on webhooks.
- ImageStep registers only
https://URLs on public hosts. A local n8n onhttp://localhost:5678cannot activate the trigger — set n8n'sWEBHOOK_URLto a public https address, a tunnel for instance. - The signature is over the raw request bytes. If a reverse proxy strips
rawBody, the node re-serialises the parsed JSON, which verifies only when byte-identical;401s in the endpoint's delivery log are the sign.
Templates
Two importable workflows ship in the npm package, under templates/ — on a self-hosted n8n installed as above, in ~/.n8n/nodes/node_modules/n8n-nodes-imagestep/templates/ — and sit next to the node in the repository, in packages/n8n-nodes-imagestep/templates/. Import one with Workflows → Import from file, then pick your own credentials, folder and sheet in each node:
- Google Drive folder → remove_bg → Google Sheets: a new file in a folder, background removed, a row with the public URL.
- Google Sheets prompt → generate → publish: the URL written back to the same row. To keep the product identical across rows, swap the generate node for Preset → Run on a consistency preset, with Input set to None and the row’s scene as its Prompt — the preset’s subjects stay fixed and each row brings its own scene.
The longer pipelines on recipes — a cut-out, a packshot and a web size from one upload, the same character on every card, the same product in every scene, a template per row — are n8n templates too, each beside a Node script that runs the same pipeline, in the open-source recipes repo.
Errors, cost and idempotency
- An API error becomes an n8n error whose message reads
ImageStep <code> (param: <name>): <message>and whose description carriesretryable. With Continue On Fail the item becomes{ error, code, param, retryable }instead of stopping the run; with Retry On Fail, branch onretryable. The codes are the closed set on errors, retries & limits. - A job that finishes with failed items is not an error:
failedItemsand each item'serrorsay what happened, and the successful outputs are still returned. - AI ops charge credits per item; deterministic ops are free on paid plans and count against the Free plan's monthly quota (pricing). Dry Run shows the exact price.
- When Wait Seconds runs out the item fails with
Job … still PROCESSINGandretryable=true: the job keeps running and is charged for what completes. Retry On Fail picks the same job back up (same key, below); with Continue On Fail the item is the job reference itself, withtimedOut: truebesideerror— branch on it and continue with Job → Wait on itsjobId. For a long batch turn Wait for Result off and continue with Job → Wait or the trigger. - Every write carries an idempotency key made from the execution, the node, the item and the request, and a
retryableanswer is tried twice more with that same key, waiting whatRetry-Aftersays. So Retry On Fail, which runs the node again over every input item, gets back the jobs an earlier try already submitted instead of creating and charging them again. A refusal is not retried, and its description carries thedetails—insufficient_credit'stopUpUrlamong them, for whoever reads the failed run. Running the workflow again is a new execution, and does the work again.
Tested against n8n's n8n-workflow 2.x API; Node 18+. Source and changelog in the repository.