We wanted to automate Instagram and LinkedIn carousel posts end-to-end: pick a topic from a curated list, generate a three-slide visual story, approve it, and publish. The content team writes topics into a database table. The agent picks the next one, generates three 9:16 images using Imagen 3, and queues the carousel for approval before it goes live.
The pipeline runs as a cron in n8n. On paper it is straightforward. In practice, three separate things broke before it worked.
Why Imagen 3 on Vertex AI
We were already calling Gemini for text generation through the standard API. Image generation was the missing piece. Imagen 3 on Vertex AI produces significantly better quality for branded content than the alternatives we tested — the 9:16 aspect ratio output is clean, the composition follows the prompt closely, and negative prompt support is strong enough to keep UI elements and watermarks out of the frames.
Vertex AI requires a Google Cloud service account rather than an API key. The authentication flow is a JWT assertion against oauth2.googleapis.com/token, which returns a short-lived Bearer token you pass to the Imagen endpoint. Token lifetime is one hour. We cache the token in n8n's $getWorkflowStaticData('global') so the workflow does not mint a new one on every loop iteration — just on expiry.
What Broke First: process.env in the Code Node
The first version of the Get Vertex Token node used process.env to read the service account file path. It worked fine when tested against the main n8n instance. It failed immediately in production with process is not defined.
The reason is that n8n's task runner — the separate process that executes Code nodes in newer versions — runs in a sandboxed V8 context that does not expose the Node.js global process object. It is not a bug. It is intentional isolation. The fix was to hardcode the path to the mounted service account file (/tmp/vertex-sa.json) and remove all process.env references from the node entirely. The file itself is mounted as a read-only volume in the Docker Compose config.
What Broke Second: $helpers in the Code Node
After removing process.env, the next failure was $helpers is not defined. The original implementation used $helpers.httpRequest() — n8n's built-in HTTP helper — to POST the JWT assertion to Google's token endpoint.
$helpers is also not available in the task runner sandbox. It exists in the legacy execution context but not in the isolated one. The fix was to switch all HTTP calls to Node's native require('https') with callback-based https.request(). This is available in the sandbox because it is a core Node module, not an n8n injection. The same pattern applies to the Imagen 3 API call — native https.request(), wrapped in a Promise for async/await compatibility.
What Broke Third: Leaking SQL from Template Syntax
The Postgres node that writes carousel results to the database used n8n's expression syntax inside a SQL string literal. When you write VALUES ('={{ $json.topic }}'), n8n evaluates the expression but the = sign from the expression opener ends up concatenated into the string value. The inserted text gains a leading = character that should not be there.
The correct pattern is to make the entire query string a single expression: the full SQL as a template literal, returned as one evaluated value. No string literal wrappers, no leaked syntax characters from mixed expression and literal content.
The Approval Flow
Generated carousels land in an approval queue in the dashboard before any post goes live. A content reviewer sees the three slide images, the caption, the hashtags, and the topic it was generated from. One click approves and queues it for publishing; another rejects it and marks the topic as available again for the next run.
The same dashboard handles both the orchestrator-triggered approval path — posts queued by the AI agent — and the dashboard-triggered path for posts composed manually. We merged these into a single unified LinkedIn tab rather than maintaining two separate views for the same platform. Agent-queued posts appear at the top with a distinct amber treatment so reviewers know they came from automation rather than a manual compose session.
The Rule for n8n Code Nodes
If you are building image generation or any external API call into an n8n Code node and hitting sandbox errors, the rule is: native Node modules only. No process, no $helpers, no assumptions about what the execution context exposes. require('fs'), require('crypto'), and require('https') all work. Build JWT assertions manually, make HTTP calls with native https.request(), and wrap callbacks in Promises for async/await flow. Testing in legacy execution mode will not surface these failures — they only appear when the task runner is active, which is the default in current n8n versions.
.png&w=384&q=75)