Gateway — Persistent Agents

SkillProductivity

Build 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.

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

FieldTypeNotes
timezonestringDefault timezone for crons (e.g., "America/New_York")
persistMemoryAcrossRunsbooleanReuse same agent ID across all gateway executions so memory spans heartbeats/crons. Default: false
heartbeat.intervalMsnumberDefault: 60,000ms (1 min)
heartbeat.policy"always"|"adaptive"|"conservative"
heartbeat.instructionstringTask prompt for each heartbeat
heartbeat.maxConsecutiveSkipsnumberStop skipping after N no-ops
crons[].schedulestringStandard cron expression
crons[].instructionstringTask prompt for this cron
crons[].priority"low"|"normal"|"high"|"critical"
policies.dailyTokenBudgetnumberHard token cap per day
policies.maxActionsPerHournumberRate limit for proactive actions
policies.requireApprovalForstring[]Tools requiring human approval
portnumberHTTP port for webhook server

GatewaySummary (from handle.stop())

FieldType
totalRunsnumber
heartbeatsFirednumber
cronChecksnumber

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 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)

Signals

GitHub stars
28
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
gateway-persistent-agents
Source
github.com/tylerjrbuell/reactive-agents-ts