Gateway — Persistent Agents
SkillProductivityBuild always-on agents with heartbeats, cron scheduling, webhook triggers, and a persistent policy engine using the Gateway layer.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Gateway — Persistent Agents skill
What this skill tells your AI
The instructions your AI receives, as published by tylerjrbuell/reactive-agents-ts in apps/docs/skills/gateway-persistent-agents/SKILL.md and read by ahel’s review.
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, // 30 minutes
policy: "adaptive", // skip if no new work
instruction: "Check for new alerts and summarize",
},
crons: [
{
schedule: "0 9 * * MON-FRI", // 9am weekdays
instruction: "Generate daily status report",
priority: "normal",
},
],
policies: {
dailyTokenBudget: 50_000,
maxActionsPerHour: 20,
},
})
.withCostTracking({ daily: 5.0 })
.withObservability({ verbosity: "normal" })
.withHealthCheck()
.build();
// Start the persistent loop
const handle = await agent.start();
// Graceful shutdown
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, // 1 hour
policy: "always", // always run regardless of activity
// policy: "adaptive" // skip if agent has nothing useful to do (default)
// policy: "conservative" // only run on explicit triggers
instruction: "Review incoming messages and respond to urgent ones",
maxConsecutiveSkips: 5, // stop skipping after 5 consecutive no-ops
}
Cron scheduling
crons: [
{
schedule: "0 9 * * 1", // Every Monday at 9am (standard cron syntax)
instruction: "Review PRs and post weekly summary to Slack",
priority: "high", // "low" | "normal" | "high" | "critical"
timezone: "America/New_York",
enabled: true,
},
{
schedule: "*/15 * * * *", // Every 15 minutes
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"],
},
]
// Gateway starts an HTTP server on gateway.port (default: varies — check builder docs)
// Incoming webhooks are normalized and passed as tasks to the agent
Policy engine
policies: {
dailyTokenBudget: 100_000, // hard stop after N tokens/day
maxActionsPerHour: 50, // rate-limit proactive actions
heartbeatPolicy: "adaptive", // global override for all heartbeats
requireApprovalFor: ["file-write", "send-email"], // tools that need human approval
}
Persistent memory across runs (episodic context across heartbeats/crons)
.withGateway({
persistMemoryAcrossRuns: true, // default: false
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 // Enables adaptive skip
consecutiveHeartbeatSkips: number // Forces execute after N skips (safety net)
tokensUsedToday: number // For daily budget enforcement
actionsThisHour: number // For rate limit enforcement
pendingEvents: GatewayEvent[] // Queued work from webhooks
}
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 callshandle.stop() - Heartbeat
intervalMsdefault 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.dailyTokenBudgetresets at midnight in thetimezonespecified — ensure timezone is set correctly- Webhook secrets must match what the external service sends — mismatch causes all webhook events to be rejected silently
persistMemoryAcrossRuns: truerequires.withMemory()with a persistent database (e.g., SQLite) to be useful — otherwise memory tiers default to in-memory and still get wiped between runsconsecutiveHeartbeatSkipsis adaptive-mode only — "always" and "conservative" modes don't use it (no skipping to reset)
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
gateway-persistent-agents- Source
- github.com/tylerjrbuell/reactive-agents-ts