Heartbeats
A heartbeat is a recurring routine an agent runs in the
background. Define them in your manifest under agents[].heartbeats.
At install time the platform materializes each heartbeat as a row in
linkworld_app_data_feeds with on_change='invoke_agent'; the
shared TenantScheduler picks them up on each 15-second tick.
The hot path:
- Scheduler finds rows where
next_run <= now()and active-hours + cooldown allow the run - Pre-filter runs (zero-LLM): a skill call or internal DB query
- If pre-filter found something (or
invoke_always: true), the target agent is invoked with the records as context - Agent’s reply is parsed for
HEARTBEAT_OK(suppressed) and the<escalate-to-vision/>marker (wakes the agent’s vision-loop) - Memory writeback for substantive ticks
- Commitment-inference post-pass
Manifest schema
Section titled “Manifest schema”agents: - id: cmo name: CMO system_prompt: You are the CMO of an AI-first marketing org. is_default: true team: [content_drafter, x_monitor] heartbeats: - slug: hourly_kpi skill: _internal_app_records tool: query params: record_type: kpi schedule_type: interval interval_minutes: 60 active_hours: start: "07:00" end: "23:00" timezone: "user" invoke_always: false cooldown_seconds: 300 conversation_mode: dedicated model_tier: cheap prompt: Review KPI delta, escalate via team.delegate if off-pace.Field reference
Section titled “Field reference”| Field | Required | Notes |
|---|---|---|
slug | yes | Unique per agent. Lowercase letter-prefixed. |
skill | yes | Pre-filter source. LOW-risk skill or internal sentinel. |
tool | yes | Skill’s tool name; for internal sources must be "query". |
params | no | Free-form dict passed to the pre-filter source. |
schedule_type | no | interval (default) / daily / weekly / cron. |
interval_minutes | required for interval | 5 .. 43 200 (30 days max). |
cron_expression | required for cron | Standard 5-field cron string. |
time_of_day | required for daily / weekly | HH:MM (24 h). |
day_of_week | required for weekly | 0=Mon … 6=Sun. |
active_hours.{start,end,timezone} | no | Skip outside this window. |
invoke_always | no, default false | When true, fires every tick (cron-style). |
cooldown_seconds | no | Minimum spacing flood guard. |
conversation_mode | no, default dedicated | dedicated / isolated / main. |
model_tier | no, default cheap | cheap / standard / premium. |
prompt | no | Per-tick instruction (≤ 8 000 chars). Empty = run on records alone. |
Pre-filter sources
Section titled “Pre-filter sources”Three kinds:
1. LOW-risk skill calls
Section titled “1. LOW-risk skill calls”Any skill whose tool action map says LOW. The platform default-denies non-LOW tools to prevent heartbeat side-effects.
heartbeats: - slug: x_mentions skill: x_api tool: search_mentions params: query: "@akundi21" interval_minutes: 302. _internal_app_records (zero-skill SQL)
Section titled “2. _internal_app_records (zero-skill SQL)”Queries the app’s own linkworld_user_app_records:
heartbeats: - slug: pending_drafts skill: _internal_app_records tool: query params: record_type: x_post interval_minutes: 240Returns the data JSONB of matching records. The pre-filter passes
records → agent if any rows match.
3. _internal_agent_memory (zero-skill memory query)
Section titled “3. _internal_agent_memory (zero-skill memory query)”Queries this agent’s memory entries:
heartbeats: - slug: review_commitments skill: _internal_agent_memory tool: query params: kind: commitment interval_minutes: 60The agent’s agent_id is auto-resolved from app_id + target_agent_slug — you don’t need to pass it.
HEARTBEAT_OK suppression
Section titled “HEARTBEAT_OK suppression”When the agent’s reply is HEARTBEAT_OK at start or end (≤ 300
chars total of remaining content), the platform:
- Doesn’t persist the message to chat history
- Just updates
last_heartbeat_ok_aton the data-feed row
This keeps the dedicated-mode conversation thread bounded (no explosion of “nothing to do” messages over weeks).
The middle-of-message HEARTBEAT_OK is not special — only
start / end positions trigger suppression.
Vision-loop escalation
Section titled “Vision-loop escalation”When the agent emits <escalate-to-vision/> anywhere in the reply,
the platform:
- Sets the agent’s vision row’s
next_action_at = now() + 30s - Keeps the heartbeat reply as a normal substantive message
Use this when the heartbeat detects something the routine layer shouldn’t decide on its own — re-planning, strategic pivots, etc. The vision-loop’s ASSESS phase will run on the next scheduler tick with full multi-phase reasoning.
Conversation modes
Section titled “Conversation modes”dedicated (default)
Section titled “dedicated (default)”Each agent gets a separate heartbeat conversation thread, distinct from its user-facing chat. HEARTBEAT_OK suppression keeps it bounded. Persists across ticks → continuity (agent remembers yesterday’s draft).
Best for: most heartbeats. Predictable token cost.
isolated
Section titled “isolated”Fresh single-shot LLM call each tick. No prior history.
Best for: high-frequency heartbeats where token cost > continuity, or pure read-only checks (KPI tracker that never needs to remember between ticks).
Heartbeats post into the user-facing main conversation. The user sees them inline.
Best for: rare cases — heartbeats whose output should be visible as part of the user chat experience (e.g. an “always-on” assistant that interleaves heartbeat-driven proactive messages with user-driven turns).
Multi-LLM tier routing
Section titled “Multi-LLM tier routing”model_tier | Resolved role |
|---|---|
cheap (default) | fast — provider’s cheap tier (Haiku / 4o-mini / Flash) |
standard | specialist — Sonnet / 4o / Pro |
premium | orchestrator — Opus / 4o / Pro depending on provider |
Per-call provider routing comes from the tenant’s LLM settings
(linkworld_tenant_settings). Heartbeat code is provider-agnostic;
the LLM client adapter picks the concrete model.
Active-hours timezone semantics
Section titled “Active-hours timezone semantics”active_hours: start: "09:00" end: "17:00" timezone: "Europe/Berlin"- IANA timezone names (
Europe/Berlin,America/New_York, …) work "user"falls back to the tenant’s configured timezone- Outside the window the heartbeat is skipped silently (no LLM, no skill call, no log spam)
- Cross-midnight windows supported (
start: 22:00end: 06:00) - Same-value start/end is rejected (zero-width window)
Phase-aligned scheduling
Section titled “Phase-aligned scheduling”Interval-mode heartbeats use a deterministic per-tenant salt
(hash(tenant_id) % min(interval, 10min)) when computing
next_run, so:
- Different tenants get different offsets → no thundering herd at xx:00:00
- Within a tenant, heartbeats with the same interval cluster within a 5-minute window → improves Anthropic prompt-cache hit rate
You don’t see the salt; it’s transparent. Just know the platform won’t fire all your tenants’ hourly heartbeats simultaneously.
Failure handling
Section titled “Failure handling”failure_count increments on every error. After
MAX_CONSECUTIVE_FAILURES = 5 the row is auto-disabled
(status='disabled'). Successful run resets to 0.
The Health screen surfaces heartbeats with failure_count >= 3 AND status='active' as failing. At 5 they move to disabled.
SDK side: there isn’t one
Section titled “SDK side: there isn’t one”Heartbeats are entirely declarative — the manifest is the SDK
API. There’s no ctx.heartbeats.* runtime call. The agent’s
behavior at heartbeat time is just a normal LLM turn (the platform
invokes the agent the same way ctx.agent.ask would).
If you want to trigger a heartbeat manually for testing, use the admin endpoint:
curl -X POST https://app.linkworld.ai/api/admin/data-feeds/{feed_id}/run-now \ -H "Authorization: Bearer ${TENANT_TOKEN}"(or the equivalent in Workspace Control’s agent detail page).
See also
Section titled “See also”- Manifest v2 — the full schema reference
- Cross-app calls —
ctx.team.delegatefor intra-app,ctx.apps.invokefor cross-app - Commitments — the post-pass after substantive heartbeat ticks
- Platform-side: Agents & heartbeats