DEV Community

Cover image for Why I Keep APX Schedules on the Runtime Side
Manuel Bruña
Manuel Bruña

Posted on

Why I Keep APX Schedules on the Runtime Side

Why I Keep APX Schedules on the Runtime Side

One of the easiest mistakes to make in an agent system is to treat a schedule like project truth.

I made that mistake more than once while building APC and APX. The intent felt project-level, so my instinct was to store it with the repo. The job itself felt durable. The reminder felt important. The whole thing looked like something the project should own.

That instinct is only half right.

The project can own the intention. The runtime has to own the execution.

That distinction sounds minor until you try to run real scheduled work across machines, restarts, and different agent surfaces. Then it becomes obvious. A schedule is not just a line in a file. It is timing, process uptime, daemon state, local logs, and delivery behavior. Those pieces do not travel cleanly with the repository.

The boundary I kept hitting

APC is the committed side of the stack. APX is the local one.

That split already tells me where the hard line should be: durable project context in the repo, runtime state on the machine. Routines exposed that boundary in a sharper way than I expected.

A routine is scheduled APX work. The daemon checks for due routines every 5 seconds. The routine itself has a kind, a schedule, optional JSON spec, and optional pre/post shell hooks. It can be a heartbeat, shell command, exec_agent, super_agent, or telegram delivery.

None of that is just a static project fact.

A schedule depends on the machine clock. A delivery step depends on local credentials and plugins. A routine history depends on what actually ran, not what the repo promised. That is runtime behavior, even when the routine is attached to a project.

That was the first useful lesson for me: if the thing needs a daemon to wake it up, it is already living in runtime land.

Why --project matters

The other thing that made this concrete was the --project flag.

APX does not pretend a routine can float free without a project. If you do not pass --project, the command falls back to the built-in scratch workspace at ~/.apx/projects/default/. That is not a bug. It is a guardrail.

I like that because it forces a decision.

If I am creating a weekly summary, or a recurring cleanup job, or a check that should belong to one repo, I need to say so. I need to bind the routine to a project on purpose. Otherwise APX assumes I am just experimenting in the shared default workspace.

That saved me from a bad habit: making automation look more portable than it really was.

A routine without a project binding often means one of two things:

  • I have not thought about where the work belongs.
  • I am about to spread machine-specific behavior into something I will later expect to clone cleanly.

The --project requirement is small, but it keeps the boundary honest.

The job of a routine is not to do everything

I also had to unlearn the idea that a routine should generate text and deliver it in one step.

APX gives you a cleaner split than that.

pre_commands can gather local data. The routine kind can produce text. post_commands can deliver the result. And if the pre-step already knows nothing needs to happen, it can emit APX_SKIP and let APX skip the model call entirely.

That split matters because it keeps side effects visible.

A routine that both thinks and sends is harder to reason about. If it fails, I have to ask whether the problem was data gathering, model output, or delivery. If it double-sends, I have to inspect the whole chain.

I hit the same kind of issue in other parts of APX too: when generation and delivery blur together, the system gets harder to trust.

For routines, the fix was simple and boring:

  1. gather the facts locally,
  2. let the model write the message if needed,
  3. let the shell or plugin deliver it.

That is not as flashy as a one-click automation story, but it is much easier to debug.

Here is the shape I now prefer:

apx routine add weekly-summary \
  --project my-app \
  --kind exec_agent \
  --schedule "every:7d" \
  --spec '{"agent":"reviewer","prompt":"Summarize the current project state in one short paragraph."}' \
  --post-commands 'apx telegram send "$APX_LLM_OUTPUT"'
Enter fullscreen mode Exit fullscreen mode

The routine still feels like one thing to me from the outside. Internally it is three smaller concerns, and that is better.

Why exec_agent usually wins

One thing I learned while working on this: super_agent is not the default answer for every routine.

If a routine needs the full APX tool registry, super_agent makes sense. But most scheduled work does not need the whole kitchen sink. When I want one project agent to produce one bounded piece of text, exec_agent is the cleaner choice.

That keeps the tool surface closer to the problem.

It also keeps the routine easier to review later. A small routine is easier to inspect, easier to change, and easier to move when the project evolves. If a recurring job is really just “summarize this repo state and send it,” I do not want it to look like a giant general-purpose agent session with side effects buried inside.

The same rule applies when I look at history. APX can show me routine history, message logs, and runtime behavior locally. I do not need to reconstruct what happened from committed repo files. That is the point.

What changed in my own thinking

The real shift was not technical. It was conceptual.

I stopped asking, “Should this schedule be part of the project?”

I started asking, “What part is project truth, and what part is machine execution?”

That question is much better.

If the answer is “this is the policy for when work should happen,” I can keep the intent near the project. If the answer is “this depends on uptime, local auth, daemon state, and the current machine,” then it belongs in APX runtime state.

That separation gives me a few practical benefits:

  • The repo stays cleaner.
  • The runtime stays honest about what it owns.
  • I can restart or move machines without pretending the schedule was portable all along.
  • I can debug a failed routine by checking the runtime history instead of hunting through committed files.

It also keeps me from overpromising. A project can describe what it wants. Only a runtime can actually wake up on time and do the thing.

That sounds obvious once you say it out loud. It took me a while to stop fighting it.

The rule I keep now

My current rule is simple:

If it needs time, delivery, or daemon state, it lives on the APX side. If it needs to survive as project meaning, it lives on the APC side.

That rule has become one of the better design filters in this whole stack.

It keeps routines boring on purpose. It keeps the repo from pretending to be a scheduler. It keeps runtime behavior local, inspectable, and replaceable.

And that is the real win for me.

I do not want APC to become a place where every automation idea goes to get buried.
I do not want APX to become a second repo full of accidental project truth.

I want a clean split: intent in the project, execution in the runtime.

That is enough structure to keep the system useful, and little enough structure to keep it sane.

Top comments (0)