Arnel ColarAll work   Get in touch

Case study ยท Telegram, n8n, Meta Graph API

A bakery's captions, drafted, approved, and posted from Telegram.

Nobody at the bakery wants to open a scheduler. They already have Telegram on the phone, so the whole workflow lives there: a draft arrives with the photo, one tap publishes it to the Facebook Page, the other tap rejects it with a reason. The bot writes the caption in the shop's own Taglish voice, and refuses to describe a cake that is not in the picture.

  • n8n
  • Telegram Bot API
  • Meta Graph API
  • Google Drive and Sheets
  • Self-hosted model gateway
chocolate-drip.jpg
Chocolate drip cake para sa maliit na celebration. Two layers ng moist chocolate cake, finished with ganache na hindi sobrang tamis. Perfect para sa birthday na hindi kailangan ng malaking handaan. Made fresh today, ready for pickup. #ChocolateDripCake #CelebrationCake #HomeBakery #FreshlyBaked
Post to FacebookNot on brand
Post to Facebook
Posted to 1 of 1 platform. facebook post 100064512881002_918447362152938 Open the post Send any message for the next one.

The brief

01 / 05

The bakery had photos, a voice, and no time. The job was not to write a scheduler, it was to make the decision to post take one tap while keeping the words and the picture honest.

How it runs

02 / 05

A queue file on the server is the source of truth. Generation happens on a timer; the approval loop is the only interactive part; publishing is a single Graph API call.

What the model is allowed to write

03 / 05

A caption is only useful if it is true. Three gates run before a draft ever reaches the phone, and a draft that fails is thrown away rather than sent with a warning.

Grounded in the photo

A vision pass describes each photo in the Drive folder once and caches the note. A caption must share at least one concrete subject with that note, so a "strawberry cake" caption cannot be attached to a chocolate one.

const shared = subject_words(caption)
  .filter(w => photo_words.has(w));

if (!shared.length) {
  reject('caption does not match the photo');
}

Written like the shop talks

Taglish, one clause per line, four PascalCase hashtags at the end, no questions. The rules are checked mechanically, so a draft cannot drift toward marketing-speak over time.

rules: words 20..80
       hashtags === 4, PascalCase
       lines >= 5, one clause each
       no question marks
       banned: "elevate", "indulge", "โ€”"

Cleaned before it is judged

Long dashes are rewritten to commas before validation, so a correct caption is not lost to a typographic habit. The same cleanup runs on redrafts and on the queue.

text.replace(/\s*[โ€”โ€“]\s*/g, ', ')

Publishing without duplicates

04 / 05

The tap is the fragile part. Telegram retries, requests time out, and an ack can arrive after the post is already live. The rules below keep one row from becoming two posts.

One call to the Page

The photo is uploaded to the Page's photos edge with the caption attached, which returns a post id. That id is what the confirmation links to.

POST /{page-id}/photos
  url      = {public image url}
  caption  = {approved caption}
  access_token = {page token}

-> { id, post_id }

Acknowledge, never abort

The tap must be answered or the button spins forever. That acknowledgement is allowed to fail without taking the rest of the run with it, because the post may already exist by then.

{
  "node": "Send ack",
  "onError": "continueRegularOutput"
}

Recover a stalled row

A row is marked Publishing before the call. If the confirmation never lands, a sweep returns it to Queued after a timeout, so the next approval starts from a known state instead of a stuck one.

if (status === 'Publishing'
    && age > 10 * 60 * 1000) {
  status = 'Queued';
}

Limits

05 / 05

What it does not do, written down so nobody assumes otherwise.

Want one for your site?

Fixed scope, fixed price, quoted before I start. You own the code, and I reply within one working day.

Send me the page and the deadline See more work