An agent goal should end.
That sounds obvious until you try to make an agent do something every morning. The tempting move is to give it a goal like:
Every day, teach me algorithms and keep a catalog.
That is not a goal. That is a job.
If you treat it as a goal, you quickly get bad semantics. Does the goal ever complete? Is the agent blocked while waiting for tomorrow? Should it burn a thread forever? What happens when you want a second daily workflow, like a newsletter draft, an infrastructure report, or an image lesson?
The fix was to stop stretching Codex goals into a scheduler.
Goals remain finite. Routines own recurrence.

The Boundary That Matters
The Mattermost bridge already had a useful shape:
Mattermost thread
-> bridge service
-> Codex app-server thread
-> streamed replies back into Mattermost
That is good for normal interactive work. You ask a question in a Mattermost thread. The bridge maps that root post to a Codex app-server thread. Future replies in the same Mattermost thread continue the same Codex conversation.
For finite goals, that model still works:
/goal fix the failing deploy script and verify the service restarts
The goal belongs to that Codex thread. It can be active, complete, blocked, or cleared. The bridge should not quietly resurrect it tomorrow.
Routines needed a separate boundary:
/routine create daily-algo --schedule daily --time 08:00 --target dm --thread new --state /home/ck/routines/algorithms/catalog.md -- Teach one algorithms lesson for today...
That command does not create a never-ending Codex goal. It creates a bridge-owned schedule.
Each run is still finite.
What the Bridge Stores
The routine state is intentionally boring JSON:
/home/ck/.local/state/mattermost-codex-bridge/routines.json
It stores the routine definition, the next scheduled time, the latest runs, and enough target information to send the result back to Mattermost:
{
"id": "daily-algo",
"enabled": true,
"schedule": {
"type": "daily",
"time": "08:00"
},
"target": {
"type": "dm",
"username": "mike"
},
"threadPolicy": "new",
"statePath": "/home/ck/routines/algorithms/catalog.md"
}
The statePath is the important part for continuity.
The scheduler should remember when to run. The agent should remember what it has taught, drafted, inspected, or generated. Those are different kinds of state, and mixing them makes the system harder to reason about.
For the algorithms routine, the durable memory is just a Markdown catalog:
/home/ck/routines/algorithms/catalog.md
That file can contain entries like:
- Topic: Prefix sums for range queries
- Summary: Taught how a prefix-sum array stores cumulative totals...
- Artifact path: `/home/ck/routines/algorithms/artifacts/2026-06-12-prefix-sums-range-queries.png`
- Retrieval question: Why does `prefix[j + 1] - prefix[i]` equal the requested range sum?
No database was needed for the first version. A file is enough until it is not.
The Run Loop
The bridge has a small scheduler loop. It wakes up on a poll interval, reads the routine store, and asks one question:
Is any enabled routine due?
If the answer is yes, it creates a Mattermost root post for the run, builds a finite prompt, and sends that prompt to Codex.
The prompt includes the routine contract:
- what to do in this run
- where to write state
- whether the target is a DM or the current channel
- whether to create a new thread or reuse an existing one
- what artifact must exist before the run should be considered good
The bridge then records the run:
{
"id": "daily-algo-mqa82mls",
"routineId": "daily-algo",
"status": "succeeded",
"trigger": "manual",
"rootId": "r6rkk49r6iyutkznksr8os5a7r",
"finishedAt": "2026-06-12T01:05:09.100Z"
}
Manual runs and scheduled runs use the same machinery. That matters because /routine run daily-algo is the test path for the schedule.
If the manual run cannot update the catalog, cannot post to the DM, or cannot attach the image, the scheduled run will not magically become healthy at 8 AM.
Why Threads Are a Policy
The thread decision is not cosmetic.
A daily lesson should usually use:
--thread new
Each run becomes a separate Mattermost root thread. That gives the day its own artifact, replies, files, and status. It also prevents a long routine from turning into one giant conversation where today’s task accidentally inherits yesterday’s transient context.
Some workflows should use:
--thread reuse
For example, a single weekly operations report thread might make sense if the whole point is to keep a running conversation. But the default should be new threads. Independent artifacts deserve independent threads.
This is one of the places where the bridge should be opinionated.
Image Generation Exposed a Real Bug
The first daily algorithms routine was too vague:
update the state path with date, topic, summary, artifact note...
That wording allowed a text-only result. The routine did exactly what it was asked to do, and the DM did not contain an image.
The second version asked for an image, but the bridge had another problem. Codex could generate an image and save it locally, but Mattermost still needed a real file upload. Text like this is not delivery:
saved: /home/ck/.codex/generated_images/.../image.png
Delivery means Mattermost has a post with file_ids, and the file metadata says it is an image:
{
"mime_type": "image/png",
"has_preview_image": true,
"post_id": "1e7syswzxprym8me4jyao7g5ty"
}
So the bridge now listens for Codex imageGeneration items, remembers the saved image paths, uploads those files through Mattermost’s files API, and attaches the uploaded file ids to the final reply.
That last detail matters. An intermediate “generated image” event buried in a thread is easy to miss. The final Codex finished reply should carry the visible artifact.
The Skill Is Part of the System
The other mistake was treating the routine instructions as tribal memory.
If I ask a future Codex session to create a routine, it should not rediscover the boundary from scratch. The routine workflow is now a Codex skill:
deploy/mattermost-codex-bridge/skills/codex-routines
The skill says:
- use routines for recurring work
- keep goals finite
- prefer
--thread newfor daily artifacts - include a
--statefile when continuity matters - make image delivery an explicit acceptance criterion
- verify Mattermost
file_idsandmime_typefor image routines
The deploy path reinstalls that skill into:
~/.codex/skills/codex-routines
That sounds small, but it changes the ergonomics. The bridge source now owns the operating instructions. The next install does not depend on some stale folder that happened to exist on my machine.
The Commands Are Small on Purpose
The routine command surface is deliberately narrow:
/routine list
/routine show daily-algo
/routine create daily-algo --schedule daily --time 08:00 --target dm --thread new --state /home/ck/routines/algorithms/catalog.md -- Teach one algorithms lesson for today...
/routine run daily-algo
/routine pause daily-algo
/routine resume daily-algo
/routine delete daily-algo
That is enough.
There is no visual workflow builder. No database migration ceremony. No distributed queue. No calendar integration. No dashboard yet.
The bridge is a Node.js service under systemd. It has a JSON store, a scheduler loop, Mattermost API calls, and a Codex app-server client.
That is not glamorous architecture. It is the right amount of architecture for one machine doing real work.
Failure Modes Worth Respecting
The first useful version still has boundaries:
A routine prompt can be underspecified.
If you want an image, say “attach it to the final Mattermost reply.” Do not say “include a visual.”
A state file can become messy.
Markdown is fine for a small catalog. If the routine becomes a product, move the state to SQLite or another structured store.
A scheduler can double-run if the store logic is careless.
The bridge records runs and computes nextRunAt; that logic has to stay conservative around restarts.
A DM is not proof of artifact delivery.
For images, check Mattermost file_ids and file info. A local path in text is not enough.
A goal is still not a routine.
If the work repeats, schedule it. If the work completes, use a goal.
Those constraints are not embarrassing. They are the system telling you where its contracts are.
The Principle
Recurring agent work needs two loops.
The outer loop belongs to the bridge:
schedule -> create run -> post result -> compute next run
The inner loop belongs to Codex:
understand this run -> use tools -> update state -> finish
Keep those loops separate and the system stays understandable. Mix them together and every “daily task” becomes an immortal goal pretending to wait politely for tomorrow.
That is the real lesson from the bridge.
An agent routine is not an agent that never stops.
It is a finite agent run, started again by boring infrastructure.


