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
The brief
01 / 05The 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.
- No new appThe owner already answers customers in Telegram all day. Approvals arrive where the conversation already is.
- No blank pageCaptions are drafted ahead of time, so the choice is approve or reject, not write something from scratch.
- No wrong cakeEvery caption is checked against a description of the actual photo before it is offered.
- No silent double postA row can only be published once, even when a tap is retried or a request times out.
How it runs
02 / 05A 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.
- DraftA cron job asks the model for a caption in the shop's voice, validates it, and adds it to the queue with its photo.
- ReviewThe bot sends the photo and the caption with two buttons, and remembers which row is on screen.
- PublishAn approve tap posts the photo to the Page, then replies with the public link.
- RecordThe row is marked posted and mirrored to a Published tab; rejections go to a Rejected tab with the reason.
What the model is allowed to write
03 / 05A 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 / 05The 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 / 05What it does not do, written down so nobody assumes otherwise.
- One photo per postEach post carries a single picture. Several shots of the same cake go out as separate posts, because albums are not built.
- Every photo has to be looked at firstA photo needs a description on file before it can be captioned, so a fresh batch waits its turn and takes a little while to become postable.
- The words are set per shopThe voice is chosen once when the bot is set up. Changing the language or the tone means rewriting the instructions it works from, not flipping a switch.
- Someone still approvesNothing goes out on its own. The owner keeps the last word, which also means nothing is posted on a day nobody checks Telegram.
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