Background
← Back to Blog

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.js script 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

  1. User/Team Prepares Content: Posts are authored and stored in scripts/posts.json.
  2. Upload to Supabase: scripts/upload-posts.ts syncs posts to the Supabase posttext table.
  3. Scheduled Trigger: GitHub Actions workflow (.github/workflows/post.yml) runs on schedule or manually.
  4. Post Selection: post.js selects an unused post for the day from Supabase.
  5. 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