Publishes the posts in your spreadsheet queue on the day each one is due, and writes the date and link back so none goes out twice.
Uses
Google Sheets
X
Post your queued content to X on schedule
Every day at 09:00, your content queue in Google Sheets is read, one post per row with the day it should go out. Working down the sheet, the rows that are due and not yet posted are published to X, and the date and a link to the live post are written back into the row.
Nothing goes out twice: a row with anything in 'Posted On' is skipped, and that marker is written before the post is published, so an interrupted run leaves the row marked rather than letting the next run send it again. Nothing is published before its date, and a run publishes at most five posts (you can change the number); any due posts beyond that wait for the next day, and the run says how many. If X refuses a post, its row is marked 'Failed' with the date and X's reason, and the run is marked failed. A post over the 280 characters X accepts (counting each link as 23, as X does), or a row with an unreadable date, is left queued and the run is marked failed, naming the row. 'Posted On' and 'Link' are the only cells ever written, so your own columns are never touched.
You need Google Sheets and X connections, and a queue tab with 'Posted On' and 'Link' as its first two columns, then a 'Post' column and a 'Publish Date' column. Fill in the spreadsheet's link and the tab's name.
queueSheetUrl to blank.Set queueSheetUrl to blank.queueTabName to blank.Set queueTabName to blank.maxPostsThisRun to 5.Set maxPostsThisRun to 5.queueSheetUrl. Save it as queue.Read the rows of queueSheetUrl. Save it as queue.queueHeaders to queue.headers.Set queueHeaders to queue.headers.headerLine to queueHeaders joined by ||.Set headerLine to queueHeaders joined by ||.headerLine doesn't start with Posted On|Link|If headerLine doesn't start with Posted On|Link|queueHeaders doesn't contain PostIf queueHeaders doesn't contain PostqueueHeaders doesn't contain Publish DateIf queueHeaders doesn't contain Publish DatetodaysDate.Get today's date and save it as todaysDate.profile.Get your account. Save it as profile.authorHandle to i/web.Set authorHandle to i/web.profileProblemIf a step below fails, save the error as profileProblemprofile.user.username and save it as foundHandle.Trim profile.user.username and save it as foundHandle.foundHandle isn't emptyIf foundHandle isn't emptyauthorHandle to foundHandle.Change authorHandle to foundHandle.Could not read your X username, so the links written back to the queue use the generic form..Show in the output: Could not read your X username, so the links written back to the queue use the generic form..postsPublished to 0.Set postsPublished to 0.published to an empty record.Set published to an empty record.dueLeft to 0.Set dueLeft to 0.problems to an empty list.Set problems to an empty list.row in queue.rows:For each row in queue.rows:rowProblemIf a step below fails, save the error as rowProblemrowNumber to row.row_number.Set rowNumber to row.row_number.row["Posted On"] and save it as postedOn.Trim row["Posted On"] and save it as postedOn.postedOn isn't emptyIf postedOn isn't emptyrow.Post and save it as postText.Trim row.Post and save it as postText.postText is emptyIf postText is emptyrow["Publish Date"] and save it as publishDate.Trim row["Publish Date"] and save it as publishDate.publishDate is emptyIf publishDate is emptydaysSinceDue to -1.Set daysSinceDue to -1.dateProblemIf a step below fails, save the error as dateProblempublishDate to todaysDate and save it as daysSinceDue.Count the days from publishDate to todaysDate and save it as daysSinceDue.rowNumber: the publish date '@publishDate' is not a date, so the post was left in the queue. Write it as 2026-09-01. to the end of problems and save it as problems.Add Row rowNumber: the publish date '@publishDate' is not a date, so the post was left in the queue. Write it as 2026-09-01. to the end of problems and save it as problems.daysSinceDue is less than 0If daysSinceDue is less than 0https?://\S+ with https://t.co/LinkLength in postText and save it as countedText.Replace the pattern https?://\S+ with https://t.co/LinkLength in postText and save it as countedText.countedText and save it as countedLength.Count the characters in countedText and save it as countedLength.countedLength is greater than 280If countedLength is greater than 280rowNumber: the post is countedLength characters as X counts them (a link counts as 23), over the 280 X accepts, so it was left in the queue. Shorten it. to the end of problems and save it as problems.Add Row rowNumber: the post is countedLength characters as X counts them (a link counts as 23), over the 280 X accepts, so it was left in the queue. Shorten it. to the end of problems and save it as problems.postsPublished is greater than or equal to maxPostsThisRunIf postsPublished is greater than or equal to maxPostsThisRundueLeft.Add 1 to dueLeft.rowNumber of queueTabName in queueSheetUrl.Overwrite row rowNumber of queueTabName in queueSheetUrl.postProblemIf a step below fails, save the error as postProblempostText. Save it as published.Post: postText. Save it as published.rowNumber: X did not publish it - postProblem.message to the end of problems and save it as problems.Add Row rowNumber: X did not publish it - postProblem.message to the end of problems and save it as problems.rowNumber of queueTabName in queueSheetUrl.Overwrite row rowNumber of queueTabName in queueSheetUrl.postsPublished.Add 1 to postsPublished.postLink to https://x.com/@authorHandle/status/@published.tweet.id.Set postLink to https://x.com/@authorHandle/status/@published.tweet.id.linkProblemIf a step below fails, save the error as linkProblemrowNumber of queueTabName in queueSheetUrl.Overwrite row rowNumber of queueTabName in queueSheetUrl.rowNumber was published (postLink) but its link could not be written back, so the row still reads 'in progress'. Paste the link into it. Google Sheets said: linkProblem.message to the end of problems and save it as problems.Add Row rowNumber was published (postLink) but its link could not be written back, so the row still reads 'in progress'. Paste the link into it. Google Sheets said: linkProblem.message to the end of problems and save it as problems.row.row_number could not be handled, so nothing was posted from it: rowProblem.message to the end of problems and save it as problems.Add Row row.row_number could not be handled, so nothing was posted from it: rowProblem.message to the end of problems and save it as problems.summaryLines to an empty list.Set summaryLines to an empty list.postsPublished post(s) from the queue. to the end of summaryLines and save it as summaryLines.Add Published postsPublished post(s) from the queue. to the end of summaryLines and save it as summaryLines.dueLeft is 1If dueLeft is 1maxPostsThisRun. to the end of summaryLines and save it as summaryLines.Add 1 due post left for tomorrow, because 'maxPostsThisRun' is maxPostsThisRun. to the end of summaryLines and save it as summaryLines.dueLeft due posts left for tomorrow, because 'maxPostsThisRun' is maxPostsThisRun. to the end of summaryLines and save it as summaryLines.Add dueLeft due posts left for tomorrow, because 'maxPostsThisRun' is maxPostsThisRun. to the end of summaryLines and save it as summaryLines.problems with a new line and save it as problemText.Join problems with a new line and save it as problemText.problems isn't emptyIf problems isn't emptyproblemText to the end of summaryLines and save it as summaryLines.summaryLines joined by a blank line.Stop and report a problem: summaryLines joined by a blank line.summaryLines joined by a blank line.Finish the run: summaryLines joined by a blank line.