Guides

Bulk import from CSV

Load a spreadsheet of posts

Planning a 30-day product launch? A month of recipes? A quarter of “Tip of the day”? Write them all in a spreadsheet, upload a CSV, and OpenPost schedules the entire campaign.

The CSV shape

Columns are case-insensitive. Only content, channels, and schedule_at are required.

FieldTypeDescription
content*stringBase post copy. Use \\n for line breaks inside a cell.
channels*stringPipe-separated list: x|linkedin|instagram.
schedule_at*ISO 8601UTC timestamp. Use 'queue' to drop into the next open slot.
media_urlURLPublic image/video URL. We download and attach before scheduling.
tagsstringComma-separated internal tags.
alt_textstringAlt text for the attached media.
thread_idstringGroup rows with the same thread_id into a single X thread.

Example CSV

launch-week.csv
content,channels,schedule_at,media_url,tags
"Day 1: The idea — 🚀",x|linkedin,2026-05-01T14:00:00Z,https://cdn.example.com/day1.jpg,"launch,day1"
"Day 2: The team behind it",x|linkedin,2026-05-02T14:00:00Z,https://cdn.example.com/day2.jpg,"launch,day2"
"Day 3: First customer result",x|linkedin,2026-05-03T14:00:00Z,,launch
"Day 4: Pricing is live",x|linkedin|instagram,2026-05-04T14:00:00Z,https://cdn.example.com/pricing.png,"launch,pricing"
"Day 5: Thanks for a wild week",x|linkedin,2026-05-05T18:00:00Z,,launch

Upload flow

1

Publishing → Bulk import

You’ll see a drop zone and an example template link.

2

Drop in the CSV

The parser runs immediately. Malformed rows are flagged red with the exact reason (missing field, unknown channel, invalid timestamp).

3

Dry run

Toggle Dry run. OpenPost simulates scheduling every row — checks character limits, media availability, queue collisions — and shows a per-row preview. Fix any issues in the CSV and re-upload.

4

Confirm and schedule

Hit Schedule. All rows are persisted atomically — either every row lands, or none do. You’ll see them appear on the calendar.

For launches I work on personally, I keep the CSV in Git. It’s a diff-able record of the campaign, easy to review in a PR, and anyone on the team can propose an edit.

Threads from a CSV

Group rows into an X thread with thread_id. Rows with the same ID become sequential tweets; order is determined by the schedule_at (or by row order if schedule matches).

http
content,channels,schedule_at,thread_id
"1/ Here's what I learned shipping Open Post for a year 🧵",x,2026-05-01T14:00:00Z,why-op
"2/ Nothing kills momentum like cross-posting by hand.",x,2026-05-01T14:00:00Z,why-op
"3/ So we built this. Simple composer, brutal previews.",x,2026-05-01T14:00:00Z,why-op
"4/ Try it: openpost.so",x,2026-05-01T14:00:00Z,why-op

Queue-aware scheduling

Instead of explicit timestamps, put queue in schedule_at. OpenPost will drop each row into the next open slot for the target channel’s queue, respecting the order in the CSV.

http
content,channels,schedule_at
"Post one",x,queue
"Post two",x,queue
"Post three",x,queue

With three X slots per week, these would schedule on the next three Tuesdays at your configured time — whatever the queue says.

Bulk import via the API

For programmatic workflows, hit POST /v1/posts/group. Same validation, same atomicity. See the posts API.

Max 500 rows per CSV. If you have more, split into multiple files or switch to the API which handles the same limit per request. There’s no cap on how many batches you can run.
Last updated April 2026 Edit this page