How Batwara Automates LinkedIn Posts: A Technical Deep Dive
Published on 2 January 2026
Automating social media posts can save time and ensure consistency. At Batwara, we use a combination of GitHub Actions, Node.js scripts, and a simple JSON file to post regularly to LinkedIn. Here’s a technical breakdown of how it all works:
1. The Workflow: .github/workflows/post.yml
This GitHub Actions workflow is scheduled to run at 10:00 AM IST every Monday, Thursday, and Sunday, or can be triggered manually. It:
- Checks out the repository
- Sets up Node.js (v18)
- Installs dependencies (like
node-fetch) - Runs the
post.jsscript with required environment variables (like the LinkedIn access token)
2. The Posts Source: scripts/posts.json
This JSON file contains a mapping of day-of-year to an array of post texts. Each day, a new post is selected based on the current day, ensuring fresh content for each scheduled run.
Example snippet:
{
"1": ["Most expense apps are built for permanent groups.\nTrips aren't. ..."],
"2": ["Friends don't want to download apps. ..."],
// ...
}
3. Uploading Posts: scripts/upload-posts.ts
This TypeScript script reads all posts from posts.json and uploads them to the Supabase posttext table, mapping each post to a specific day of the year. It can be run manually or as part of a deployment pipeline. The script loads environment variables, reads the JSON, and uses the Supabase client to insert or update posts.
Example: Generic TypeScript Script
// scripts/upload-posts.ts (generic)
import { createClient } from '@supabase/supabase-js';
import posts from './posts.json';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
async function uploadPosts() {
for (const [day, postArray] of Object.entries(posts)) {
for (const text of postArray) {
await supabase.from('posttext').upsert({
day_of_year: Number(day),
text,
});
}
}
console.log('Posts uploaded!');
}
uploadPosts();
Example: SQL Table Definition
Below is a generic SQL script to create the posttext table in Supabase:
create table posttext (
id serial primary key,
day_of_year integer not null,
text text not null,
used boolean default false,
used_at timestamp with time zone
);
4. Posting Logic: post.js
post.js: This script is the entry point for the GitHub Action. It calls the local server endpoint, which:- Fetches an unused post for today
- Posts it to LinkedIn: fetches the user's profile, then posts the content using the LinkedIn API, authenticating with the access token from secrets.
- Marks it as used
5. The Cron Job: GitHub Actions Schedule
The cron schedule in post.yml ensures posts go out at the right time, even if no one is online. Manual dispatch is also supported for flexibility.
Summary
This setup allows Batwara to:
- Maintain a rotating set of posts
- Easily update or add new posts via JSON
- Automate posting with minimal manual intervention
- Keep secrets and tokens secure via GitHub Actions
Technical Deep Dive: LinkedIn Automation in Batwara
Architecture Overview
Batwara automates LinkedIn posts using a combination of scheduled GitHub Actions, Node.js scripts, Supabase, and the LinkedIn API. The process is designed to be secure, extensible, and easy to maintain.
Key Components
- GitHub Actions: Orchestrates scheduled and manual runs.
- Node.js Scripts: Handles post selection, API calls, and orchestration.
- Supabase: Stores post content, usage logs, and LinkedIn tokens.
- LinkedIn API: Publishes posts on behalf of Batwara.
Sequence of Operations
- User/Team Prepares Content: Posts are authored and stored in
scripts/posts.json. - Upload to Supabase:
scripts/upload-posts.tssyncs posts to the Supabaseposttexttable. - Scheduled Trigger: GitHub Actions workflow (
.github/workflows/post.yml) runs on schedule or manually. - Post Selection:
post.jsselects an unused post for the day from Supabase. - Logging: The post is marked as used in Supabase, and results are logged.
Sequence Diagram
sequenceDiagram
participant User
participant GitHubActions
participant NodeScript as Node.js Scripts
participant Supabase
participant LinkedIn
User->>NodeScript: Prepare posts.json
NodeScript->>Supabase: Upload posts (upload-posts.ts)
GitHubActions->>GitHubActions: Scheduled/manual trigger
GitHubActions->>NodeScript: Run post.js
NodeScript->>Supabase: Fetch unused post for today
LinkedIn-->>NodeScript: API response
NodeScript->>Supabase: Mark post as used, log result
NodeScript->>GitHubActions: Success/failure status
Component Diagram
graph TD
A[GitHub Actions] -->|Runs| B[post.js]
B -->|Selects post| C[Supabase]
B -->|Posts to| D[LinkedIn API]
C -->|Stores| E[Posts, Usage Logs]
D -->|Publishes| F[LinkedIn Feed]
API/Data Flow Diagram
flowchart TD
subgraph Batwara Automation
A1[posts.json] --> A2[upload-posts.ts]
A2 --> A3[Supabase posttext table]
B1[GitHub Actions] --> B2[post.js]
B2 -->|Fetch post| A3
B2 -->|Send post| C1[LinkedIn API]
C1 -->|Response| B2
B2 -->|Mark used| A3
end
Security Considerations
- LinkedIn tokens are stored securely in Supabase or GitHub Secrets.
- Only authorized scripts can trigger posts.
- All API calls are authenticated and rate-limited.
Error Handling
- LinkedIn API failures are logged and surfaced in GitHub Actions logs.
- Token expiration triggers a notification for re-authentication.
If you want to automate your own social media posts, this stack is a simple, extensible, and serverless way to do it!
All scripts are open-sourced here:
https://github.com/nishantmendiratta/linkedin-post-automation
