Most newsletters die the same way. The first six issues go out on time, then one Friday you have nothing ready, you skip it, and the gap becomes a month. Writing is rarely the problem. The problem is the weekly deadline with nobody holding it.
This recipe gives the deadline to Codex, and keeps the decision with you. Through the week you toss links and half-ideas into one file. On Thursday afternoon a scheduled task turns them into a draft issue. You read it, fix a line, and flip one word to approved. On Friday morning a second task publishes it to your Substack, and only if you approved it.
When you’re done you’ll have:
- a
newsletterfolder withnotes.md(your scratchpad),voice.mdand anissues/folder holding one file per issue, - a Thursday task that drafts and never publishes,
- a Friday task that publishes exactly one approved file and does nothing otherwise,
- a
sent.mdlog with the URL of every issue.
Setup takes about 20 minutes, most of it signing in and one supervised first run.
Who this fits: a solo writer or small team with their own Substack publication, on a computer that is on and has the ChatGPT desktop app running on Friday morning. Substack has no official way to post from outside its own editor (more on that in this explainer), so the post goes out through your own signed-in Substack session, on your machine, like one you’d click by hand.
The recipe
Keep this handy; you’ll paste it into Codex in step 2:
Read https://www.publishport.app/skill.md and follow it to set up PublishPort for me, then check that my Substack account is connected. In this folder, create notes.md, voice.md and an issues/ folder. Write a first issue as issues/<next Friday's date>.md with a front-matter block (status: draft, title, subtitle, send: no) followed by plain-text paragraphs, built from notes.md. Show it to me, and once I approve, save it to my Substack as a draft (not published, no email) so I can see it arrive. After that, help me create two Codex scheduled tasks: a Thursday 5 pm task that drafts next Friday's issue from notes.md and my last three issues without publishing anything, and a Friday 8 am task that publishes today's issue only if its status is approved.
The skill file is a short public instruction sheet. It tells Codex how to install itself, ask you for one authorization click, check which accounts are connected, publish, and confirm the post exists. Every AI you authorize is listed on your account page, where you can revoke it.
Step 0: Get three things ready
1. The ChatGPT desktop app (Codex lives inside it). Install it from OpenAI’s download page and sign in. Per OpenAI’s docs, scheduled tasks are created and managed in the desktop app or on the web, and the Codex CLI and IDE extension have no Scheduled interface. Tasks that work on a local folder need the computer on and the app running.
2. The PublishPort desktop app, signed in to Substack. Install it from the download page, open Platform accounts, click Sign in next to Substack, sign in the normal way in the window that opens, then click I’ve signed in (walkthrough).
3. Node.js, an empty folder and a few notes. The publishing command runs on Node.js. Check with node -v, or install the LTS release from nodejs.org. Create a folder called newsletter. Optionally add a voice.md with two of your past issues you like and a few “never say” rules; drafts match your voice far better with it. You can also leave it empty and let the first supervised run teach the AI your voice.
Step 1: What the Substack command can and can’t do
Check this before you promise yourself anything (checked against the PublishPort command set, publishport-opencli 1.0.40):
| Job | Supported | Worth knowing |
|---|---|---|
| Publish a post with title and subtitle | Yes | The body is plain text; a blank line starts a new paragraph |
| Images | Yes | Local paths or URLs; they are added after the text |
| Email subscribers | Yes, only with an explicit send option | Off by default: the post goes on the site, no email |
| Save as draft only | Yes | Nothing goes public; open it in Substack’s editor to finish |
| Paid-only audience, file under a section | Yes | Section is matched by name |
| Choose a publish time on Substack’s side | No | The timing comes from the scheduled task on your computer |
| Bold, headings, links inside the text | No | A pasted URL stays plain text |
The plain-text limit shapes the recipe. If your issues need headings, bold and inline links, use the draft option: the AI puts the text on Substack as a draft and you do the final formatting and press publish there. If your issues are mostly paragraphs and a few images, the fully automatic route works as written.
Step 2: Supervised first run
Open the ChatGPT desktop app, switch to Codex, and open the newsletter folder as your project. It doesn’t need to be a Git repository; in non-version-controlled projects scheduled tasks run directly in the project directory. Paste the recipe. What happens:
-
Codex installs the PublishPort skill and runs the login. It gives you an authorization link and a confirmation code. Open the link, sign in or create a PublishPort account, check the code matches, click Authorize. The link lasts 10 minutes; ask for a new one if it expires.
-
It checks your computer and tells you whether Substack shows as signed in. If not, sign in again under Platform accounts and ask it to re-check.
-
It creates the files and writes the first issue. The top of each issue file looks like this:
--- status: draft title: What I learned shipping the pricing page subtitle: Four rewrites, one surprise send: no --- First paragraph. Second paragraph. -
It shows you the text. Edit anything (“shorter opening”, “drop the last paragraph”), then approve.
-
It saves the issue to Substack as a draft (not published, no email). Open your Substack dashboard and you’ll see it. This is your proof that the whole chain works before anything public happens.
Codex will ask you to approve a few commands. That is its default safety behavior; approve them while you’re watching.
Step 3: Let scheduled tasks reach the network (once)
Skip this and Friday’s run fails at the publish step. Scheduled tasks run unattended with your default sandbox settings and don’t stop to ask (OpenAI docs), and the default workspace-write sandbox blocks network access, which every publish needs.
OpenAI’s answer is rules: allow one command prefix outside the sandbox rather than opening everything. Send this in the same chat and approve what it asks:
Install the publishport CLI globally with npm, then add a rule to ~/.codex/rules/default.rules that allows commands starting with "publishport" to run outside the sandbox. Tell me when to restart the app.
By hand: run npm install -g publishport, then add this to .codex/rules/default.rules in your home folder:
prefix_rule(
pattern = ["publishport"],
decision = "allow",
justification = "PublishPort publishing needs network access",
)
Quit and reopen the ChatGPT desktop app so the rule loads. Only commands starting with publishport run outside the sandbox.
Step 4: Create the two scheduled tasks
Codex will offer to create them from the recipe; OpenAI’s docs confirm you can create a task by describing it in chat. Open Scheduled in the sidebar and check what’s there against the prompts below. To build by hand, create a standalone task (each run starts fresh from the saved prompt), pick the newsletter project and set the time.
Task 1: newsletter-draft, weekly, Thursday 5 pm:
Draft next Friday's newsletter issue. Do not publish anything and do not contact Substack.
1. Read notes.md, voice.md (if present) and my last three files in issues/ so the new issue matches my voice and doesn't repeat their topics.
2. Create issues/<date of next Friday>.md. If that file already exists, stop and tell me; never overwrite it.
3. Start the file with front matter: status: draft, a title, a one-line subtitle, send: no. Then 400 to 700 words of plain-text paragraphs separated by blank lines: no markdown, no headings, no bullet symbols. Use only facts and links that are in notes.md. Do not invent statistics, quotes or stories.
4. Put any link as a bare URL on its own line.
5. Tell me in two lines what the issue covers and which notes you used.
Task 2: newsletter-publish, weekly, Friday 8 am:
This is a pre-approved publishing task. Publish without asking me to confirm, but only as the rules below allow.
1. Open issues/ and find the file named with today's date. If there is none, or sent.md already has a line for today, stop and say so.
2. Read its front matter. If status is not exactly "approved", do nothing and say it wasn't approved.
3. Check the time. If it is after noon, stop and say the run was late.
4. Check publishport guide substack for the live options, then publish the title, subtitle and body (not the front matter) to my Substack. Email subscribers only if the file says send: yes; otherwise publish to the site without emailing.
5. If the command errors or times out, do not run it again. Look at my Substack's latest posts first and retry once only if today's post isn't there.
6. On success set status to published in the file, append the date and the post URL to sent.md, and give me a one-line summary.
The split is deliberate. The draft task cannot publish, so a bad week costs you an edit. The publish task invents nothing: it posts only the file you approved, and only if the word approved is there. Each scheduled run starts cold and nobody is watching, so the permission to publish has to be written into the prompt, which PublishPort’s instructions otherwise treat as “show the content first”.
OpenAI recommends testing a prompt by hand and reviewing the first few runs. You tested the chain in step 2; after Thursday’s first run just read the draft in issues/, and after Friday’s open Scheduled for the result.
The weekly rhythm
- During the week: paste links, observations and half-sentences into
notes.md. Ten minutes in total is plenty. - Thursday evening or Friday morning: open the new file in
issues/, edit, changestatus: drafttostatus: approved. Changesend: notosend: yesif this issue should also go to subscribers’ inboxes. - Friday 8 am: it publishes and logs the URL. If you changed nothing, nothing goes out, which is the right failure.
Keep send: no for your first few automated issues and look at them on the site. Emailing is the part you can’t take back.
If something doesn’t run
- Nothing happened on Friday: the computer was asleep or the ChatGPT app wasn’t running. Local scheduled tasks need both. The prompt skips after noon so a late catch-up can’t surprise you.
- The publish step reports a network error: revisit step 3. Check the rule file, restart the app, and make sure the task says
publishport, notnpx. - Substack says you’re signed out: sessions expire. Sign in again under Platform accounts in the PublishPort app.
- The AI says no device is online: the PublishPort app isn’t running. It starts with your computer by default; start it by hand if not.
- Headings and bold are missing on the site: the command sends plain text. Use the draft option and finish formatting in Substack.
- The Thursday task says the file already exists: that’s the overwrite guard doing its job. Rename or delete the old one if you want a fresh draft.
Taking it further
Once the issue is out, the next chore is telling people about it. Promote a newsletter issue on social media shows how to have the same AI turn each published issue into posts for your other platforms. If you want the AI running in the cloud instead of on your desktop, the Claude Code scheduled tasks recipe covers that setup.
The free plan covers setting all of this up; plan limits and pricing are on the pricing page.
