---
title: "n8n Instagram auto-post workflow (free template)"
description: "Auto-post to Instagram from n8n with Meta's API: Instagram Login, HTTP Request nodes, a status check instead of a fixed wait, and a template to import."
url: https://rizamo.com/guides/n8n-instagram-auto-post-workflow/
---

# n8n Instagram auto-post workflow (free template)

n8n can publish to Instagram on a schedule with nothing but its own core nodes and Meta's official API. This guide builds that workflow: a trigger, three HTTP Request nodes, a wait loop that checks the post is ready, and a stop when it is not. The template at the end imports in one paste.

It follows the same API calls as our [Instagram API guide](/guides/how-to-post-to-instagram-via-api/), where each call was run on a real account. Read that one for the details of each call; this one is about doing it well in n8n.

## The short version

1. An Instagram professional account and a Meta app with Instagram Login. No Facebook Page needed.
2. A 60-day token from the App Dashboard, stored in n8n as a Header Auth credential.
3. In n8n: Schedule Trigger, a node with the image URL and caption, then **Create container**, **Wait**, **Check status**, and **Publish** once the status is `FINISHED`.
4. Something that renews the token before day 60, and something that marks each post as done.

## Before you start

- **The account.** Meta's API only serves [professional accounts](https://developers.facebook.com/documentation/instagram-platform/overview), business or creator.
- **The app.** Create a Meta app with the Instagram product and Instagram Login. If you only post to accounts you own or manage, Standard Access is enough and there is [no App Review](https://developers.facebook.com/documentation/instagram-platform/app-review).
- **The token.** The **Generate token** button in the App Dashboard gives you a token for your own account that lasts 60 days ([get started](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/get-started)). It needs `instagram_business_basic` and `instagram_business_content_publish`.
- **The account id.** Call `GET https://graph.instagram.com/v26.0/me?fields=user_id,username` with the token and keep `user_id`. The publishing calls take `user_id`, not `id`.
- **Public media.** Meta downloads each file from its URL, so the image or video must be on a [publicly accessible server](https://developers.facebook.com/documentation/instagram-platform/content-publishing) when the workflow runs. Images must be JPEG. A link that opens a preview page, such as a cloud drive share link, is not the file.

## Which n8n node to use

n8n's built-in **Facebook Graph API** node sends requests to `graph.facebook.com` or `graph-video.facebook.com` only ([n8n docs](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.facebookgraphapi/)). Many n8n Instagram tutorials use it, which ties them to Facebook Login and a Facebook Page linked to the account.

With Instagram Login the host is `graph.instagram.com`, so this guide uses the **HTTP Request** node. It is also easier to read: every call is a URL and a JSON body you can paste into curl to debug.

| | Instagram Login + HTTP Request | Facebook Login + Facebook Graph API node |
| --- | --- | --- |
| Needs a Facebook Page | No | Yes, linked to the Instagram account |
| Host | `graph.instagram.com` | `graph.facebook.com` |
| Upload a video file instead of a URL | No | Yes, resumable upload |
| Delete a post later | No | Yes |

Sources: Meta's [overview](https://developers.facebook.com/documentation/instagram-platform/overview) and [content publishing](https://developers.facebook.com/documentation/instagram-platform/content-publishing). If you need deletes or file uploads, use Facebook Login; the flow below stays the same with the other host.

## Store the token as a credential

Do not type the token into the URL of each node: exported workflows and shared screenshots carry it with them. Create one credential and select it on every HTTP Request node.

1. In an HTTP Request node, set **Authentication** to **Generic Credential Type** and **Generic Auth Type** to **Header Auth**.
2. Create a new credential: **Name** `Authorization`, **Value** `Bearer <your token>` ([n8n docs](https://docs.n8n.io/integrations/builtin/credentials/httprequest/)).

Meta's publishing examples send the token in that same header ([content publishing](https://developers.facebook.com/documentation/instagram-platform/content-publishing)).

## The workflow, node by node

| Node | Type | What it does |
| --- | --- | --- |
| Every day at 9:00 | Schedule Trigger | Starts the run |
| Post | Edit Fields (Set) | Holds `image_url` and `caption`. Replace it with your source |
| Create container | HTTP Request | `POST /<IG_ID>/media` with the URL and caption |
| Wait 30 s | Wait | Gives Meta time to fetch and process the file |
| Check status | HTTP Request | `GET /<container id>?fields=status_code` |
| Ready? | If | `status_code` is `FINISHED` |
| Keep waiting? | If | Still `IN_PROGRESS` and fewer than 10 checks: back to Wait |
| Not published | Stop and Error | Fails the run with the status, so you hear about it |
| Publish | HTTP Request | `POST /<IG_ID>/media_publish` with the container id |

### Create container

**Method** `POST`, **URL** `https://graph.instagram.com/v26.0/<IG_ID>/media`, **Send Body** on, **Body Content Type** JSON, **Specify Body** Using JSON:

```js title="JSON body (expression)"
{{ JSON.stringify({ image_url: $json.image_url, caption: $json.caption }) }}
```

`JSON.stringify` matters. Captions hold quotes, line breaks and emoji, and a body built by pasting `"{{ $json.caption }}"` into a JSON template breaks on the first quote. Meta answers with the container id: `{"id":"17889455560051444"}`.

### Wait, then check

Meta processes every container in the background, images included, and getting an id back does not mean the file worked. Publish only when the container's `status_code` is `FINISHED` ([IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media)). In [our Instagram API test](/guides/how-to-post-to-instagram-via-api/#how-publishing-works), an image published right after its container was created failed with `9007` / `2207027` (not ready), and worked seconds later.

A fixed Wait of one minute, which many templates use, works for small images and fails on the day a video takes longer. A loop does not:

- **Check status:** `GET https://graph.instagram.com/v26.0/{{ $('Create container').first().json.id }}?fields=status_code`
- **Ready?:** `{{ $json.status_code }}` is equal to `FINISHED`. True goes to Publish.
- **Keep waiting?:** `{{ $json.status_code }}` is equal to `IN_PROGRESS` and `{{ $runIndex }}` is less than `9`. True goes back to Wait; false goes to Stop and Error.

`$runIndex` counts how many times the node has run in this execution, from 0 ([n8n docs](https://docs.n8n.io/code/builtin/n8n-metadata/)), so the loop gives up after 10 checks, five minutes in all. That matches Meta's advice to check [once per minute, for no more than 5 minutes](https://developers.facebook.com/documentation/instagram-platform/content-publishing), checked twice as often. A Wait under 65 seconds keeps the execution in memory instead of saving it to the database ([n8n docs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/)).

The other values are final: `ERROR` (the container's `status` field holds the subcode), `EXPIRED` (not published within 24 hours) and `PUBLISHED` ([IG Container](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-container)).

### Publish

**Method** `POST`, **URL** `https://graph.instagram.com/v26.0/<IG_ID>/media_publish`, JSON body:

```js title="JSON body (expression)"
{{ JSON.stringify({ creation_id: $('Create container').first().json.id }) }}
```

The `id` it returns is the published post. Save it next to the row it came from, so the next run skips that post.

## Do not turn on Retry On Fail for Publish

n8n can retry a failed node, and for Create container and Check status that is fine. For Publish it is how you post twice: a call that timed out may have published. Before trying again, read the container's status, and if it is `PUBLISHED`, the post is live ([IG Container](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-container)).

A success can also be partial. `media_publish` can return the post id with `config_issue` set to `CAPTION_NOT_ATTACHED` or `USER_TAGGING_FAILURE`: the post is live without its caption or tags, and Meta says not to publish it again ([media publish](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media_publish)). Add an If on `{{ $json.config_issue }}` after Publish if you want to hear about it.

## Reels instead of images

Change the Create container body. Reels take longer to process, so set the Wait to 60 seconds and the `$runIndex` limit in Keep waiting? to `4`: five checks, a minute apart.

```js title="JSON body for a reel (expression)"
{{ JSON.stringify({ media_type: "REELS", video_url: $json.video_url, caption: $json.caption, share_to_feed: true }) }}
```

Reels are MP4 or MOV, up to 300 MB and 15 minutes ([IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media)). Carousels and stories use the same loop with a different body: see [carousels](/guides/how-to-post-to-instagram-via-api/#post-a-carousel) and [stories](/guides/how-to-post-to-instagram-via-api/#post-a-story) in the API guide.

## Where the posts come from

The template keeps the post in one Edit Fields node so it runs as soon as you import it. In practice, replace that node with your queue:

- **Google Sheets or Airtable:** a row per post with the media URL, caption, date and a status column. Read the first row that is due and not posted; after Publish, write the post id and the status back. Without that write, the next run posts the same row again.
- **An RSS feed:** RSS Feed Trigger, then build the caption from the item's title and description.
- **An AI node for the caption:** keep a person in the loop for a while, for example a Slack or email approval before Create container. Captions take up to 2,200 characters, 30 hashtags and 20 @ tags ([IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media)).

## Keep the token alive

The token from the dashboard lasts 60 days. It can be refreshed once it is at least 24 hours old, and each refresh gives another 60 days; a token left to expire cannot be refreshed ([Business Login for Instagram](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/business-login)).

```http title="Refresh the token"
GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=<LONG_LIVED_TOKEN>
```

A second workflow on a Schedule Trigger every few weeks can call it; then put the new token in the Header Auth credential. Until that is automated, keep a calendar reminder: after day 60 every run fails with code `190`.

## Limits to plan for

- **Posts per day:** each account has a publishing limit in a rolling 24 hours, and Meta's pages give 50 in some places and 100 in others. Read it from `GET /<IG_ID>/content_publishing_limit?fields=quota_usage,config` ([reference](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/content_publishing_limit)). Our test account returned 100 in October 2026.
- **Containers:** at most 400 created per account in 24 hours, and each expires after 24 hours ([IG User Media](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media#limitations)). A loop that creates containers and fails before publishing uses them up.
- **Errors:** the ones you will meet most are in the [error table](/guides/how-to-post-to-instagram-via-api/#common-errors) of the API guide. Code `190` means the token expired or was revoked.

## The template

Copy this, open a new workflow in n8n and paste it on the canvas. Then replace `<IG_ID>` in the three HTTP Request nodes, select your Header Auth credential on each, and put a public JPEG URL in the Post node.

We imported this template into n8n 2.15 and ran it against a real business account in October 2026, with Instagram Login. A 1600x1000 JPEG was `FINISHED` at the first check, even with the Wait set to 1 second. A 20-second reel, with the body from "Reels instead of images" and the Wait cut to 5 seconds for the test, was `IN_PROGRESS` at four checks, went back through Keep waiting? each time, and was `FINISHED` and published at the fifth: the loop is there for video.

```json title="n8n-instagram-auto-post.json"
{
  "name": "Instagram auto-post",
  "nodes": [
    {
      "id": "986cc635-0ed6-432e-be1d-2126c2b8f8b0",
      "name": "Every day at 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0],
      "parameters": { "rule": { "interval": [{ "triggerAtHour": 9 }] } }
    },
    {
      "id": "2a861e94-f258-46a7-9fbf-ae9de2c3595e",
      "name": "Post",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [220, 0],
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "13a43bf8-aa14-43e9-82b5-96e855381478", "name": "image_url", "value": "https://example.com/photos/autumn-menu.jpg", "type": "string" },
            { "id": "6554cc9b-50d7-4dd2-af99-e965cbd96da4", "name": "caption", "value": "Autumn menu is here. #coffee", "type": "string" }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "788bbddf-072c-4b7d-90e8-c973ca48b9a2",
      "name": "Create container",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, 0],
      "parameters": {
        "method": "POST",
        "url": "https://graph.instagram.com/v26.0/<IG_ID>/media",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ image_url: $json.image_url, caption: $json.caption }) }}",
        "options": {}
      }
    },
    {
      "id": "01daf579-5921-4648-9bd2-e10e20dda449",
      "name": "Wait 30 s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [660, 0],
      "parameters": { "resume": "timeInterval", "amount": 30, "unit": "seconds" }
    },
    {
      "id": "68696cd3-87b2-4570-97fa-b6f3dabb8042",
      "name": "Check status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [880, 0],
      "parameters": {
        "url": "=https://graph.instagram.com/v26.0/{{ $('Create container').first().json.id }}?fields=status_code",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      }
    },
    {
      "id": "8f7d120c-4ca8-4075-a064-b30837b61f1d",
      "name": "Ready?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1100, 0],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "e884cf4f-2085-4f6d-8962-ce22bbf635b0", "leftValue": "={{ $json.status_code }}", "rightValue": "FINISHED", "operator": { "type": "string", "operation": "equals" } }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "cb88af8b-406f-49f2-82be-b2672079f1eb",
      "name": "Keep waiting?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1320, 200],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "1010e9ea-fc74-44a9-accb-c4808f8f1c84", "leftValue": "={{ $json.status_code }}", "rightValue": "IN_PROGRESS", "operator": { "type": "string", "operation": "equals" } },
            { "id": "365b894b-1d83-466a-b5bd-7298ab22fc92", "leftValue": "={{ $runIndex }}", "rightValue": 9, "operator": { "type": "number", "operation": "lt" } }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "a3f0c2d1-5b7e-4c9a-8d6f-2e1b0c9d8a7f",
      "name": "Not published",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [1540, 300],
      "parameters": { "errorMessage": "={{ 'Container ' + $('Create container').first().json.id + ' is ' + $json.status_code }}" }
    },
    {
      "id": "b4e1d3c2-6a8f-4d0b-9e7a-3f2c1d0e9b8a",
      "name": "Publish",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1320, -100],
      "parameters": {
        "method": "POST",
        "url": "https://graph.instagram.com/v26.0/<IG_ID>/media_publish",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ creation_id: $('Create container').first().json.id }) }}",
        "options": {}
      }
    }
  ],
  "connections": {
    "Every day at 9:00": { "main": [[{ "node": "Post", "type": "main", "index": 0 }]] },
    "Post": { "main": [[{ "node": "Create container", "type": "main", "index": 0 }]] },
    "Create container": { "main": [[{ "node": "Wait 30 s", "type": "main", "index": 0 }]] },
    "Wait 30 s": { "main": [[{ "node": "Check status", "type": "main", "index": 0 }]] },
    "Check status": { "main": [[{ "node": "Ready?", "type": "main", "index": 0 }]] },
    "Ready?": {
      "main": [
        [{ "node": "Publish", "type": "main", "index": 0 }],
        [{ "node": "Keep waiting?", "type": "main", "index": 0 }]
      ]
    },
    "Keep waiting?": {
      "main": [
        [{ "node": "Wait 30 s", "type": "main", "index": 0 }],
        [{ "node": "Not published", "type": "main", "index": 0 }]
      ]
    }
  },
  "settings": {}
}
```

The template posts one item per run, which is why the expressions read `$('Create container').first()`. For several posts in one run, put a Loop Over Items node with a batch size of 1 around the nodes from Create container to Publish ([n8n docs](https://docs.n8n.io/flow-logic/looping/)).

## When n8n alone is the better choice

For one or two accounts you own, this workflow is all you need: Standard Access takes no review, and n8n's core nodes cost nothing extra. What grows with more accounts is the work around it: a token per account to refresh, a Meta app review to serve accounts you do not own, a public host for every file, and a count against each account's daily limit.

## Where Rizamo fits

Rizamo is a social media API and MCP server built to take that layer off your hands: one call to publish, with Rizamo creating the container, waiting for it, publishing it and refreshing the tokens. In n8n it is called with the HTTP Request node, or its MCP tools go on the AI Agent node ([n8n on Rizamo](/agents/n8n/)). Rizamo is opening soon: [join the waitlist](/#waitlist).
