| name | gateway-persistent-agents |
| description | Build always-on agents with heartbeats, cron scheduling, webhook triggers, and a persistent policy engine using the Gateway layer. |
| compatibility | Reactive Agents TypeScript projects using @reactive-agents/* |
| metadata | {"author":"reactive-agents","version":"2.0","tier":"capability"} |
Gateway — Persistent Agents
Agent objective
Produce a builder with .withGateway() correctly configured for the persistence pattern needed, with the agent started and gracefully stopped.
When to load this skill
- Agent must run continuously or on a schedule without human triggers
- Building a cron-based automation agent
- Agent responds to webhook events from external systems
- Agent needs daily token/action budgets and policy enforcement
Implementation baseline
import { ReactiveAgents } from "@reactive-agents/runtime";
const agent = await ReactiveAgents.create()
.withName("monitor")
.withProvider("anthropic")
.withReasoning({ defaultStrategy: "reactive", maxIterations: 8 })
.withTools({ allowedTools: ["web-search", "http-get"] })
.withGateway({
heartbeat: {
intervalMs: 1_800_000,
policy: "adaptive",
instruction: "Check for new alerts and summarize",
},
crons: [
{
schedule: "0 9 * * MON-FRI",
instruction: "Generate daily status report",
priority: "normal",
},
],
policies: {
dailyTokenBudget: 50_000,
maxActionsPerHour: 20,
},
})
.withCostTracking({ daily: 5.0 })
.withObservability({ verbosity: "normal" })
.withHealthCheck()
.build();
const handle = await agent.start();
process.on("SIGINT", async () => {
const summary = await handle.stop();
console.log(`Ran ${summary.totalRuns} times, ${summary.heartbeatsFired} heartbeats`);
process.exit(0);
});
Key patterns
Heartbeat policies
heartbeat: {
intervalMs: 3_600_000,
policy: "always",
instruction: "Review incoming messages and respond to urgent ones",
maxConsecutiveSkips: 5,
}
Cron scheduling
crons: [
{
schedule: "0 9 * * 1",
instruction: "Review PRs and post weekly summary to Slack",
priority: "high",
timezone: "America/New_York",
enabled: true,
},
{
schedule: "*/15 * * * *",
instruction: "Check for new support tickets and categorize them",
priority: "normal",
},
]
Webhook triggers
webhooks: [
{
path: "/github/webhook",
adapter: "github",
secret: process.env.GITHUB_WEBHOOK_SECRET,
events: ["push", "pull_request"],
},
]
Policy engine
policies: {
dailyTokenBudget: 100_000,
maxActionsPerHour: 50,
heartbeatPolicy: "adaptive",
requireApprovalFor: ["file-write", "send-email"],
}
Persistent memory across runs (episodic context across heartbeats/crons)
.withGateway({
persistMemoryAcrossRuns: true,
heartbeat: { intervalMs: 60_000, instruction: "Check for new work" },
crons: [{ schedule: "0 9 * * *", instruction: "Daily summary" }],
})
.withMemory({ tier: "enhanced", dbPath: "./memory.sqlite" })
When persistMemoryAcrossRuns: true, the agent reuses the same stable agentId across all gateway executions (heartbeats, crons, webhooks). This allows the memory layer to maintain episodic context across runs — the agent "remembers" what it saw in the previous heartbeat when processing the next cron, avoiding repeated work and building narrative continuity.
Without persistence (default): Each gateway execution gets a unique agentId like agent-name-heartbeat-1234567890, so memory is isolated per run. Good for stateless checks.
With persistence: All executions share agent-name, so memory spans across runs. Good for agents that need to understand history, avoid duplication, or provide narrative context (e.g., digest monitors, status trackers).
GatewayOptions reference
| Field | Type | Notes |
|---|
timezone | string | Default timezone for crons (e.g., "America/New_York") |
persistMemoryAcrossRuns | boolean | Reuse same agent ID across all gateway executions so memory spans heartbeats/crons. Default: false |
heartbeat.intervalMs | number | Default: 60,000ms (1 min) |
heartbeat.policy | "always"|"adaptive"|"conservative" | |
heartbeat.instruction | string | Task prompt for each heartbeat |
heartbeat.maxConsecutiveSkips | number | Stop skipping after N no-ops |
crons[].schedule | string | Standard cron expression |
crons[].instruction | string | Task prompt for this cron |
crons[].priority | "low"|"normal"|"high"|"critical" | |
policies.dailyTokenBudget | number | Hard token cap per day |
policies.maxActionsPerHour | number | Rate limit for proactive actions |
policies.requireApprovalFor | string[] | Tools requiring human approval |
port | number | HTTP port for webhook server |
GatewaySummary (from handle.stop())
| Field | Type |
|---|
totalRuns | number |
heartbeatsFired | number |
cronChecks | number |
State Tracking (advanced)
The gateway maintains zero-LLM-cost state that powers all policy decisions:
type GatewayState = {
lastExecutionAt: Date | null
consecutiveHeartbeatSkips: number
tokensUsedToday: number
actionsThisHour: number
pendingEvents: GatewayEvent[]
}
Inspect state at runtime: const status = await agent.gatewayStatus()
For a complete guide to state tracking, policy evaluation, and how state drives decisions, see the Gateway State Tracking Reference.
Pitfalls
.withGateway() alone does nothing — you must call .start() on the built agent to begin the loop
- Gateway holds the Node process open — always register a shutdown handler (
SIGINT, SIGTERM) that calls handle.stop()
- Heartbeat
intervalMs default is 60,000ms (1 min) — set a longer interval for agents that don't need frequent checks
- Cron expressions follow standard 5-field format (
min hour dom month dow) — verify with a cron parser before deploying
policies.dailyTokenBudget resets at midnight in the timezone specified — ensure timezone is set correctly
- Webhook secrets must match what the external service sends — mismatch causes all webhook events to be rejected silently
persistMemoryAcrossRuns: true requires .withMemory() with a persistent database (e.g., SQLite) to be useful — otherwise memory tiers default to in-memory and still get wiped between runs
consecutiveHeartbeatSkips is adaptive-mode only — "always" and "conservative" modes don't use it (no skipping to reset)