Why Station exists
Agents need a clock and a queue. Station is the background jobs framework we built so they could have one without a hosted control plane.

Every agent demo has the same shape. A prompt goes in, a model thinks, tools fire, an answer comes out, and everyone claps. Then someone asks the question that ends the demo: what happens when nobody is watching?
An agent that only runs while a request is open is not autonomous. It is a very expensive form handler. Autonomy starts the moment the system does something on its own schedule, retries when the world says no, survives the process it was started in, and leaves a record of what it did. That is not an agent problem. It is a background jobs problem. It has been a background jobs problem since before any of us wrote our first cron line.
Station is our answer to it. This is why we built our own rather than reaching for one that already existed.
The existing options work. That is not the same as being right.
There are two families of background job systems in the TypeScript world, and we have used both in anger.
The self-hosted queues, Bull, BullMQ, Agenda, are solid. They also want a Redis or a MongoDB before you have written your first job. That is a Docker container, a connection string, a thing to monitor, and a thing to explain to the next engineer, all before the job does anything. For a team of two shipping an agent, that overhead is not a rounding error. It is the difference between having background jobs and not having them.
The managed services, Trigger.dev, Inngest, are pleasant. They also put your job definitions, your inputs, and your run history on someone else's servers, behind someone else's API, on someone else's bill. For a lot of agent workloads, the input to a job is the sensitive thing. A support ticket. A customer's spreadsheet. The transcript of a voice call. Shipping that to a third-party control plane so it can be retried is a decision, and we did not want it to be the default one.
Station's landing page puts the comparison in one line: existing solutions work, they also come with trade-offs Station doesn't. SQLite by default, shared adapters when you scale. One process or a fleet. Runs on your servers with no hosted control plane. Same reliability: retries, timeouts, concurrency.
Schema in, handler out
The unit of Station is the signal. A signal is a background job with a Zod input schema, a handler, and the operational envelope every job eventually needs.
import { signal, z } from "station-signal";
export const sendDigest = signal("send-digest")
.input(z.object({ userId: z.string() }))
.timeout(30_000)
.retries(2)
.env("RESEND_API_KEY")
.run(async (input) => {
await mailer.sendDigest(input.userId);
});
That is the whole thing. The schema validates every trigger before it enters the queue, so a malformed payload fails at the door instead of in the handler. The timeout is enforced because the handler runs in an isolated child process, so a hung job actually stops. The retries back off exponentially. The .env() line makes the presence of a secret a hard precondition, and the secret itself is injected over private IPC rather than the spawn environment, so it never shows up in a process listing.
Every run is recorded: input, output, error, timing, attempt count. Not as an add-on. As the thing the framework does.
When one signal is not enough, you chain them into a broadcast, a directed acyclic graph with fan-out, fan-in, and guards. When you need the wiring to change without a deploy, a dynamic broadcast is the same DAG as JSON, edited in the dashboard, versioned, and snapshotted at trigger time so an edit never mutates a run already in flight. The rule we wrote for ourselves: if you need new code, write a signal; if you need a new wiring of existing signals, edit a broadcast.
The part nobody plans for: the process that should not end
Here is where agents diverge from ordinary background work, and where Station earned its keep.
A voice session is a long-lived, stateful duplex connection. It cannot live in a serverless function, and a bare node server.js gives you nothing when it falls over. A polling loop that watches an inbox for an agent is the same shape. So is the process that keeps a mesh of agents warm so they can answer each other without cold-start latency.
Station calls these beacons. A beacon is a supervised long-running process with a restart policy, exponential backoff, heartbeat stall detection, a startup timeout, and graceful shutdown. It is the primitive under every "agent that is always on."
In Glove's voice example, a room is a long-lived signal run: one call, one child process, up to an hour. Because the room is the process and not the socket, it outlives the page. The user can refresh, the tab can die, the conversation is still there. Each delegation to a sub-agent is another signal, a short job with its own timeout, retries, and a durable run record. We did not design that architecture and then look for a job system. The job system made the architecture obvious.
One Station when you are small. A network when you are not.
The thing we are proudest of is that none of the above changes when you grow.
A Station starts as one process with a SQLite file. When that is not enough, you swap the adapter for Postgres, MySQL, or Redis, and put a Headquarters in front of a fleet of execution stations. Headquarters serves the API and dashboard, owns the schedules, and routes work. It executes nothing. The stations advertise capacity and labels, claim work atomically, and hold leases with fencing tokens so an orphaned run is recovered rather than duplicated. Draining a station for a deploy is a flag, not a runbook.
Your signal definitions do not change. The trigger was always decoupled from execution, and that seam is the whole design.
Where Station sits in the mission
We describe Cradle Research as building primitives for autonomous systems. Station is the least glamorous of them and probably the most load-bearing.
Glove is deliberate about what it does not do. Its memory docs list, as out of scope, "triggering, scheduling, or pipeline orchestration," and then name Station as whose territory that is. Embedding backfill for memory search is done by a separate process, typically a Station signal. Glove's own subprocess runtime for agents, the one that wakes an agent cold, runs a turn, and puts it back to sleep, is explicitly modeled on Station's signal.
Glove is the agent. Station is the clock and the queue. We could have hidden that boundary inside one framework and it would have demoed better. We think it is more useful as two parts that each do one thing, that you can adopt separately, and that you can read.
Five minutes to your first signal. Then it runs while you sleep, and that is the whole point.

