apps/docs/skills/gateway-persistent-agents/SKILL.md
Build always-on agents with heartbeats, cron scheduling, webhook triggers, and a persistent policy engine using the Gateway layer.
npx skillsauth add tylerjrbuell/reactive-agents-ts gateway-persistent-agentsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Produce a builder with .withGateway() correctly configured for the persistence pattern needed, with the agent started and gracefully stopped.
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);
});
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
}
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",
},
]
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
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
}
.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).
| 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 |
handle.stop())| Field | Type |
|-------|------|
| totalRuns | number |
| heartbeatsFired | number |
| cronChecks | number |
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.
.withGateway() alone does nothing — you must call .start() on the built agent to begin the loopSIGINT, SIGTERM) that calls handle.stop()intervalMs default is 60,000ms (1 min) — set a longer interval for agents that don't need frequent checksmin hour dom month dow) — verify with a cron parser before deployingpolicies.dailyTokenBudget resets at midnight in the timezone specified — ensure timezone is set correctlypersistMemoryAcrossRuns: 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 runsconsecutiveHeartbeatSkips is adaptive-mode only — "always" and "conservative" modes don't use it (no skipping to reset)development
Orient to the Reactive Agents framework, understand the builder API shape, and select the right capability skills for your task.
testing
Enable output verification (hallucination detection, semantic entropy, self-consistency), add post-run verification steps, and run LLM-scored evals across 5 quality dimensions.
data-ai
Configure per-provider behavior, understand streaming quirks, and use the 7-hook adapter system for optimal performance across LLM providers.
data-ai
Configure the 4-layer memory system with SQLite/FTS5/vec storage for persistent agent knowledge that survives sessions.