Skip to content
Back to skills

Gateway Persistent Agents

ASecurity

Build always-on agents with heartbeats, cron scheduling, webhook triggers, and a persistent policy engine using the Gateway layer.

  • 28 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
ai-agentstypescriptgosqlreactnodeexpressgitdatabase

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tylerjrbuell/reactive-agents-ts --skill gateway-persistent-agents --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Gateway Persistent Agents?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Gateway Persistent Agents
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tylerjrbuell-gateway-persistent-agents/badge)](https://www.skillsdirectory.com/skills/tylerjrbuell-gateway-persistent-agents)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

```ts
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

```ts
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

```ts
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

```ts
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

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

```ts
.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:

```ts
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](../../../../docs/gateway-state-tracking.md).

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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.