Post to LinkedIn automatically, every day, for free. No paid scheduler, no LinkedIn Premium, no server, no monthly fee. Your machine can be off — it all runs on GitHub's free infrastructure.
You write your posts as plain text files. auto_linkedin handles the rest:
picks the next post each day, generates a clean image card, publishes it to
LinkedIn, and drops your link in the first comment (where LinkedIn doesn't
penalise it).
posts/01.txt ──► [ daily GitHub Action ] ──► your LinkedIn feed
meta/01.json ──► generated image card ──► + link in first comment
Every "schedule your LinkedIn posts" tool wants a subscription. None of the moving parts here actually cost money:
| Piece | Cost |
|---|---|
| GitHub Actions (the scheduler) | Free — unlimited on public repos |
| LinkedIn API (posting + comment) | Free — no Premium required |
| Image cards (Pillow) | Free |
So this is a free, self-hosted alternative you fully own.
- ✅ Posts one queued post per day — or two a day (morning + evening slots)
- ✅ Generates a branded dark image card per post (5 layouts, 4 accent colours)
- ✅ Adds your link/source as the first comment (better reach than a body link)
- ✅ Reliable: each window front-loads retries + fallbacks to survive GitHub's flaky cron, and a per-slot guard means it never double-posts
- ✅ Timezone-aware "day" (
TZ_OFFSET_MINUTES) so scheduling follows your clock - ✅ Runs entirely in the cloud — your computer can be off
It does not write your posts for you (that needs an AI/human). You supply the text; it handles posting. Simple and free.
Click "Use this template" → Create a new repository (or fork it). Make it private if you like — private repos still get plenty of free Actions minutes.
- Go to https://www.linkedin.com/developers/apps → Create app. You must attach a LinkedIn Page; a free placeholder Page is fine.
- On the Auth tab, copy your Client ID and Client Secret.
- Under Authorized redirect URLs, add exactly:
http://localhost:8000/callback - On the Products tab, add "Sign In with LinkedIn using OpenID Connect" and "Share on LinkedIn" (usually approved instantly).
cp config.example.json config.json # paste your Client ID + Secret into it
python3 get_token.py # opens a browser → click Allow
This writes tokens.json with your access token and your person_urn.
In your repo → Settings → Secrets and variables → Actions, add:
| Secret | Value (from config.json / tokens.json) |
|---|---|
LINKEDIN_CLIENT_ID |
your client id |
LINKEDIN_CLIENT_SECRET |
your client secret |
LINKEDIN_ACCESS_TOKEN |
access_token from tokens.json |
LINKEDIN_ACCESS_EXPIRES_AT |
access_expires_at from tokens.json |
LINKEDIN_PERSON_URN |
person_urn from tokens.json |
LINKEDIN_REFRESH_TOKEN |
refresh_token (leave empty if none) |
Edit brand.json with your name + tagline (shown in every card's footer).
In .github/workflows/post.yml:
- Edit the
cron:lines — times are UTC; the default targets ~08:00 IST. Keep the window pattern (several attempts + fallbacks): GitHub's free cron regularly delays or skips single slots, and the window is what makes posting reliable. The script guarantees it still posts at most once per slot. - Set
TZ_OFFSET_MINUTESto your UTC offset in minutes (IST 330, CET 60, EST −300, PST −480) so "one post per day" follows your day.
Push your changes. Add your posts (below), and it posts one per day. Test immediately with Actions → Daily LinkedIn post → Run workflow.
Uncomment the EVENING window cron block in post.yml and set
SLOT_BOUNDARY_UTC_HOUR to any UTC hour between your two windows. Each window
("am" / "pm") posts at most once per day, so you get exactly two posts daily —
never doubles. (Heads-up: on LinkedIn, two posts/day can compete with each
other for reach; many people do better with one strong post.)
Each post is three files sharing a number:
posts/NN.txt— the post text (posted in order, lowest number first).meta/NN.json— describes the image card (see below). Optional; omit for text-only.comments/NN.txt— the first comment, e.g. a link. Optional.
Generate/preview a card locally:
pip install Pillow
python3 render_card.py 01 # writes posts/01.png
python3 render_card.py --all
accent: green · blue · purple · amber. layout is one of:
- stat — big number:
{"layout":"stat","accent":"green","tag":"// note","headline":"...","stat":"6.5x","stat_label":"..."} - statement — big bold claim:
{"layout":"statement","accent":"amber","tag":"// take","headline":"...","sub":"..."} - list — numbered points:
{"layout":"list","accent":"blue","tag":"// tips","headline":"...","items":["...","..."]} - compare — two columns:
{"layout":"compare","accent":"purple","tag":"// vs","headline":"...","left":{"title":"A","lines":["..."]},"right":{"title":"B","lines":["..."]}} - news — headline + figure:
{"layout":"news","accent":"blue","tag":"// news","date":"2026-06","headline":"...","stat":"10x","stat_label":"..."}
The included posts/01 and posts/02 are examples — replace them with your own.
- State:
state.jsontracks what's posted so nothing repeats. The Action commits it back after each run. - Token expiry: LinkedIn access tokens last ~60 days (personal apps usually
don't get a refresh token). When posting stops, re-run
python3 get_token.pyand update theLINKEDIN_ACCESS_TOKEN/LINKEDIN_ACCESS_EXPIRES_ATsecrets. GitHub emails you when a run fails, so you'll know. - Queue empty? When you run out of posts, it simply stops until you add more.
- Timing: GitHub cron is best-effort — expect posts within the window, not to the exact second.
MIT — do whatever you want with it. Built by Poojith Devan. If it's useful, a ⭐ is appreciated.