Skip to content

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:

  1. Scheduler finds rows where next_run <= now() and active-hours + cooldown allow the run
  2. Pre-filter runs (zero-LLM): a skill call or internal DB query
  3. If pre-filter found something (or invoke_always: true), the target agent is invoked with the records as context
  4. Agent’s reply is parsed for HEARTBEAT_OK (suppressed) and the <escalate-to-vision/> marker (wakes the agent’s vision-loop)
  5. Memory writeback for substantive ticks
  6. Commitment-inference post-pass
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.
FieldRequiredNotes
slugyesUnique per agent. Lowercase letter-prefixed.
skillyesPre-filter source. LOW-risk skill or internal sentinel.
toolyesSkill’s tool name; for internal sources must be "query".
paramsnoFree-form dict passed to the pre-filter source.
schedule_typenointerval (default) / daily / weekly / cron.
interval_minutesrequired for interval5 .. 43 200 (30 days max).
cron_expressionrequired for cronStandard 5-field cron string.
time_of_dayrequired for daily / weeklyHH:MM (24 h).
day_of_weekrequired for weekly0=Mon … 6=Sun.
active_hours.{start,end,timezone}noSkip outside this window.
invoke_alwaysno, default falseWhen true, fires every tick (cron-style).
cooldown_secondsnoMinimum spacing flood guard.
conversation_modeno, default dedicateddedicated / isolated / main.
model_tierno, default cheapcheap / standard / premium.
promptnoPer-tick instruction (≤ 8 000 chars). Empty = run on records alone.

Three kinds:

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: 30

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: 240

Returns 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: 60

The agent’s agent_id is auto-resolved from app_id + target_agent_slug — you don’t need to pass it.

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_at on 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.

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.

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.

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

model_tierResolved role
cheap (default)fast — provider’s cheap tier (Haiku / 4o-mini / Flash)
standardspecialist — Sonnet / 4o / Pro
premiumorchestrator — 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:
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:00 end: 06:00)
  • Same-value start/end is rejected (zero-width window)

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

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:

Terminal window
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).