Skip to content

Content table queue

A table is the natural place to plan content with other people: one row per post, columns for the file, the text, where and when. This recipe reads such a table and turns each row into a queued post. It is written for n8n because that is where most people keep such flows, with the same calls as a script at the end. The file column holds a public https URL (a shared drive link, an S3 object), so the upload step is POST /media/from-url and no bytes pass through the workflow.

  • A paid plan and an API key with write, stored as an n8n Header Auth credential - name Authorization, value Bearer ds_live_xxxxxxxxxxxxxxxxxxxx - never in a node’s body.
  • A sheet or CSV with the columns file_url, text, title, accounts (account ids separated by |, from GET /accounts), when (a local time, 2026-09-29T18:00, read in the workspace’s time zone), status (empty until done).
  • n8n with the Google Sheets node, or a CSV file.
Google Sheets: Read rows (status is empty)
→ HTTP Request: POST /media/from-url → mediaId
→ Wait 20 s → HTTP Request: GET /media/{{mediaId}} → loop until status = ready
→ HTTP Request: POST /posts/plan → stop the row if ok = false
→ HTTP Request: POST /posts (confirm: true) → post ids
→ Google Sheets: Update row (status = queued, post_ids)
  1. Read rows. Google Sheets node, operation Get many rows, filter status empty.

  2. Import the file. HTTP Request node, POST https://api.dropslate.top/v1/media/from-url, credential Header Auth, body JSON:

    { "url": "{{ $json.file_url }}", "filename": "{{ $json.file_url.split('/').pop() }}" }

    The answer is { "mediaId": "med_…", "status": "processing" }.

  3. Wait until ready. A Wait node of 20 seconds, then HTTP Request GET https://api.dropslate.top/v1/media/{{ $('Import').item.json.mediaId }}, then an IF node on {{ $json.status === 'ready' }} that loops back to the Wait node when false. A failed status ends the row with failReason written to the sheet.

  4. Plan. HTTP Request POST https://api.dropslate.top/v1/posts/plan, body:

    {
    "targets": { "accountIds": {{ JSON.stringify($('Read rows').item.json.accounts.split('|')) }} },
    "mediaIds": ["{{ $('Import').item.json.mediaId }}"],
    "title": "{{ $('Read rows').item.json.title }}",
    "text": "{{ $('Read rows').item.json.text }}",
    "when": { "publishAt": "{{ $('Read rows').item.json.when }}", "timezone": "Europe/Kiev" }
    }

    An IF node on {{ $json.ok }}: false routes the row to a sheet update with status = check and {{ $json.table }} in a notes column - a person reads which account and why.

  5. Create. The same body plus "confirm": true to POST https://api.dropslate.top/v1/posts. The answer has posts[]; write {{ $json.posts.map(p => p.id).join('|') }} and status = queued back to the row.

Set Batching on the plan and create nodes to one item per second: the workspace allows 60 POST /posts an hour, and a sheet of 100 rows would otherwise hit rate_limited at row 61 - the node’s retry-on-fail with the Retry-After header handles the rest.

Terminal window
export KEY="ds_live_xxxxxxxxxxxxxxxxxxxx"; export API="https://api.dropslate.top/v1"; AUTH="Authorization: Bearer $KEY"
tail -n +2 posts.csv | while IFS=, read -r file_url text title accounts when; do
MEDIA=$(curl -s -X POST "$API/media/from-url" -H "$AUTH" -H "Content-Type: application/json" -d "{\"url\":\"$file_url\"}" | jq -r .mediaId)
until [ "$(curl -s "$API/media/$MEDIA" -H "$AUTH" | jq -r .status)" = "ready" ]; do sleep 10; done
BODY=$(jq -n --arg a "$accounts" --arg m "$MEDIA" --arg t "$text" --arg ti "$title" --arg w "$when" \
'{targets:{accountIds:($a|split("|"))}, mediaIds:[$m], text:$t, title:$ti, when:{publishAt:$w, timezone:"Europe/Kiev"}}')
PLAN=$(curl -s -X POST "$API/posts/plan" -H "$AUTH" -H "Content-Type: application/json" -d "$BODY")
[ "$(echo "$PLAN" | jq -r .ok)" = "true" ] || { echo "$PLAN" | jq -r .table; continue; }
curl -s -X POST "$API/posts" -H "$AUTH" -H "Content-Type: application/json" -d "$(echo "$BODY" | jq '.+{confirm:true}')" | jq -r '.posts[].statusLine'
done

Every row with status = queued has a post on the Calendar at its when; GET /posts?status=queued lists them with the same statusLine. Rows marked check are the ones a person has to fix - usually text over a network’s limit or a file the content type does not take.

  • Per-network text columns. Add text_threads, text_facebook and put them in overrides when not empty.
  • A group column instead of account ids: "targets": {"groupId": "{{ $json.group }}"}.
  • Make or Zapier. The same five HTTP calls; Make and Zapier show the module and action names.