Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

← All guides

Playbook subscription guide

Official Kody guide

Use playbook subscriptions when a saved playbook should react to Kody-owned event topics. The saved playbook remains the top-level entity; subscriptions are nested manifest metadata and playbook runtime handlers.

Manifest shape

Declare subscriptions in package.json#kody.subscriptions as a record keyed by event topic:

{
	"name": "@scope/email-automation",
	"exports": {
		".": "./src/index.ts"
	},
	"kody": {
		"id": "email-automation",
		"description": "Automates stored inbound email.",
		"subscriptions": {
			"email.message.received": {
				"handler": "./src/on-email-message-received.ts",
				"description": "Process stored inbound mail."
			}
		}
	}
}

Each subscription definition supports:

  • handler (required): playbook-local module path for the event handler.
  • description (optional): human-readable purpose for playbook detail and subscription listings.
  • filters (optional): topic-specific metadata reserved for dispatchers.

Save-time checks normalize handler paths and build derived bundle artifacts for subscription handlers. Runtime dispatch invokes the handler through the normal playbook execution path with playbook context, playbook-owned storage, playbook-owned secrets, and kody:runtime.

Discovery

Use search for playbook subscription work, then call the built-in playbookSubscriptionsList capability to inspect the signed-in user's declared subscriptions:

{
	"topic": "email.message.received"
}

The result lists playbook id, kody.id, playbook name, topic, handler, description, and filters. Use this before debugging event dispatch, building fan-out, or deciding whether a playbook already subscribes to a topic.

Synthetic dispatch

playbookSubscriptionDispatch invokes one subscription handler on one saved playbook over MCP. It is a platform-marked real-surface run with real side effects. Use it immediately after saving to verify handler wiring without waiting for production fan-out.

{
	"kody_id": "email-automation",
	"topic": "email.message.received",
	"params": {}
}

For stored inbound mail, replay with email_message_id instead of params:

{
	"kody_id": "email-automation",
	"topic": "email.message.received",
	"email_message_id": "00000000000000000000000000000001"
}

Pass exactly one of params or email_message_id. There is no caller idempotency_key — the platform generates internal idempotency keys.

Use playbookGet and playbookSubscriptionsList to inspect current handlers. Save edits as complete files with expected_edit_token from the current read.

Handler guidance

  • Platform markers. The platform sets top-level synthetic: true and, for stored-mail replay, replay_of. Real event dispatch strips caller-supplied synthetic and replay_of from handler envelopes. Run records agree with the handler payload.
  • params or email_message_id. Fixture params merge into the handler envelope before markers are added. email_message_id rebuilds the stored inbound email envelope from D1.
  • Treat synthetic identically to production. Handlers run the same code path unless a deliberately visible irreversible-side-effect guard says otherwise.
  • Start minimal. Begin with {} or the smallest object your handler accepts, then add fields until the smoke test covers the branches you care about.
  • Filters are not applied. Production dispatch for playbook-emitted topics skips subscribers when filters do not match the payload; synthetic dispatch always runs the named playbook. Put filter-matching fields inside params when testing filter-dependent code paths.
  • Admin-only topics (email.system-message.received, platform.feedback.submitted, status.incident.opened, fleet.playbook_error_rate.elevated, fleet.entitlement.crossed, auth.denial.burst, email.delivery.burst, status.incident.resolved, user.created, user.deleted, user.email_verification.failed, user.email_verification.stalled, user.email_outbound.paused, email.system-message.sent) gate production fan-out on admin role; synthetic dispatch still runs your handler directly for smoke testing.
  • Activity. Synthetic runs appear on the subscription surface. Handler failures do not emit run.error.recorded (recursion guard).

Full call semantics and examples: Synthetic event dispatch.

Playbook-emitted topics (@scope/...)

Playbooks can define their own event topics and emit to them; every other playbook saved by the same user that declares the topic in kody.subscriptions receives the event. There is no cross-user delivery.

Declaring emitted topics

Declare topics in package.json#kody.emits. Topics must use the scoped form @{username}/topic.name with a lower-dot-case body, and the scope must match the emitting playbook's npm scope:

{
	"name": "@kentcdodds/discord-gateway",
	"kody": {
		"id": "discord-gateway",
		"description": "Discord gateway.",
		"emits": {
			"@kentcdodds/discord.message.created": {
				"description": "A Discord message was created.",
				"payloadSchema": {
					"type": "object",
					"properties": {
						"messageId": { "type": "string", "minLength": 1 },
						"channelId": { "type": "string" }
					},
					"required": ["messageId", "channelId"],
					"additionalProperties": false
				}
			}
		}
	}
}

payloadSchema is optional. When present it must be a JSON Schema subset with root "type": "object"; supported keywords are type, description, properties, required, additionalProperties (boolean), items, enum, const, minLength, maxLength, minimum, maximum, minItems, and maxItems. Unsupported keywords fail playbook checks at save time so authors never rely on silently ignored constraints. Declared schemas appear in playbook search/detail projections so subscribers can discover payload shapes.

Emitting

Emit from any playbook runtime context (exports, subscription handlers, playbook-owned jobs, apps, retrievers) with the events helper:

import { events } from 'kody:runtime'

await events.dispatch({
	topic: '@kentcdodds/discord.message.created',
	idempotencyKey: `discord:message-create:${message.id}`,
	payload: { messageId: message.id, channelId: message.channelId },
})

Rules:

  • The topic must be declared in the emitting playbook's kody.emits.
  • idempotencyKey is required; payloads must be JSON objects and are validated against payloadSchema when declared.
  • Payloads are capped at 64 KiB (canonical JSON). Store large data with playbookStorage() and emit a reference instead.
  • events.dispatch is unavailable in ad hoc execute runs — topics belong to playbooks, so emit from playbook code (or statically import a playbook export that dispatches).

Delivery semantics

Dispatch is asynchronous and durable: events.dispatch validates the event, enqueues it on the kody-playbook-events-dispatch Queue (with DLQ), and returns { topic, source, idempotencyKey, status: "enqueued" } immediately. Emitters never observe subscriber results or latency; check each subscriber's run records for handler outcomes.

The Queue consumer resolves the emitting user's subscribed playbooks at delivery time and invokes each subscription:@scope/topic handler with:

type PlaybookEventEnvelope = {
	event: string
	source: { type: 'playbook'; playbook_id: string; kody_id: string }
	idempotency_key: string
	payload: Record<string, unknown>
}
  • Per-subscriber invocations are exactly-once keyed on (source playbook, subscriber playbook, topic, idempotencyKey), so Queue redelivery replays stored results instead of re-running handlers.
  • Infrastructure failures before handler code runs retry via the Queue (3 attempts, then the kody-playbook-events-dispatch-dlq dead-letter queue). Terminal handler failures do not retry — a stored failed invocation replays rather than re-running — and stay visible in run records.
  • Event-driven chains carry a nested invocation depth budget (max 8 hops), so emit cycles between playbooks terminate.
  • In environments without the Queue binding (local dev, preview) — or when an enqueue fails — dispatch falls back to inline delivery with the same consumer code path and reports status: "delivered_inline" instead of "enqueued".

Filters on playbook-emitted topics

A subscription to a playbook-emitted topic may declare filters; every filter key must be present in the event payload with an equal JSON value or the subscriber is skipped:

{
	"kody": {
		"subscriptions": {
			"@kentcdodds/discord.message.created": {
				"handler": "./src/on-general-chat-message.ts",
				"filters": { "channelId": "1470913684598423592" }
			}
		}
	}
}

Platform-owned topics (below) keep their existing behavior: their dispatchers define whether and how filters apply.

email.message.received

Accepted stored inbound email dispatches email.message.received after Kody stores the message and attachment metadata. Quarantined mail uses email.message.quarantined instead.

Handlers receive a metadata-first payload:

type EmailMessageReceivedEvent = {
	event: 'email.message.received'
	message: {
		id: string
		inbox_id: string | null
		from_address: string | null
		envelope_from: string | null
		to_addresses: Array<string>
		cc_addresses: Array<string>
		reply_to_addresses: Array<string>
		subject: string | null
		message_id_header: string | null
		in_reply_to_header: string | null
		references: Array<string>
		processing_status: 'stored' | 'sent' | 'failed'
		received_at: string | null
		created_at: string
	}
	attachments: Array<{
		id: string
		filename: string | null
		content_type: string | null
		content_id: string | null
		disposition: string | null
		size: number
		storage_kind: string
		storage_key: string | null
		created_at: string
	}>
}

Do not expect parsed bodies or attachment bytes in the event. Fetch full message bodies, parsed headers beyond the event metadata, or attachment bytes only when the handler needs them with emailMessageGet, emailAttachmentGet, or the playbook runtime email helper.

email.message.quarantined

Quarantined stored inbound email dispatches email.message.quarantined instead of email.message.received. The payload matches email.message.received with event: 'email.message.quarantined'. Reclassifying a message later does not retroactively dispatch either topic.

email.message.delivery.updated

Outbound Email Sending lifecycle changes dispatch email.message.delivery.updated. The payload contains metadata for the owned Kody message plus the provider event id, delivery status, terminal flag, recipient, SMTP delivery fields, optional bounce/failure/rejection/complaint details, and provider event timestamp.

Use this topic for delivery notifications and bounce or complaint workflows. Do not resend on deferred: Cloudflare still has provider retries pending. Provider event ids are stored idempotently, so duplicate Queue delivery does not dispatch duplicate playbook invocations. Out-of-order events remain available in delivery history but do not dispatch after a newer status.

email.system-message.received (admins)

Accepted mail stored in the operator-owned system inbox (kody@<apex>, support@<apex>, and the other reserved system locals) dispatches email.system-message.received to playbooks saved by users who hold the admin role at dispatch time. Quarantined system-inbox mail is stored but never dispatched. Non-admin subscribers never receive system mail.

The payload matches email.message.received (with event: 'email.system-message.received') plus an admin_url string linking to the stored message in the admin interface (/admin/system-email?messageId=...). Handlers run as the admin playbook owner, so the user-scoped email capabilities and the email runtime helper cannot read the system message — use the metadata and admin_url for notifications, and the admin adminSystemEmailGet capability for full contents.

email.system-message.sent (admins)

A successful adminSystemEmailSend / sendSystemEmail fans email.system-message.sent to playbooks saved by users who hold the admin role at dispatch time. This includes sends from the @kentcdodds/system-email utility and raw capability calls. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next send.

There is no Queue / DLQ for this topic. Dispatch is best-effort after the provider send already succeeded: a failed invoke is logged and does not fail the send or refund the daily cap.

Outbound system mail is not stored on the dedicated inbound system_email_* graph (that graph refuses provider-message-id rows), so this topic carries the sent correspondence itself — recipients, subject, text, and HTML — for admin archive playbooks. It is the documented exception to metadata-only admin topics.

Handlers receive:

type SystemEmailSentEvent = {
	event: 'email.system-message.sent'
	from: string
	to: Array<string>
	subject: string
	text: string | null
	html: string | null
	reply_to: string | null
	provider_message_id: string | null
	sent_at: string
}

Idempotency keys include the topic, provider message id (or sent_at when the provider omitted one), and subscriber playbook id. Deduplicate in playbook storage on provider_message_id when a utility already recorded the same send.

platform.feedback.submitted (admins)

A successful, consent-gated metaPlatformFeedbackSubmit insert enqueues a durable platform.feedback.submitted attempt. The Queue consumer dispatches to playbooks saved by users who hold the admin role when the message is processed. A non-admin playbook may declare the topic, but it never receives the event. Admin roles are read fresh for every attempt, so revocation stops delivery on the next processed submission.

Handlers receive the explicitly approved feedback and attributed submitter identity:

type PlatformFeedbackSubmittedEvent = {
	event: 'platform.feedback.submitted'
	content_warning: string
	admin_url: string
	feedback: {
		id: string
		category: 'friction' | 'bug' | 'experience' | 'suggestion' | 'other'
		status: 'open'
		created_at: string
		summary_untrusted: string
		details_untrusted: string
	}
	submitter: {
		user_id: string
		username: string | null
		email: string | null
	}
}

summary_untrusted and details_untrusted are the exact feedback the user explicitly approved. They remain user-authored untrusted data, and content_warning tells handlers to treat them as feedback rather than instructions. admin_url is built from the trusted deployment origin and links to /admin/platform-feedback?feedbackId=<encoded id>, making it suitable for an admin notifier. The event also includes the submitter's account user id, username, and email snapshot stored with the submission. Retries never resolve mutable live profile data, so an intervening account profile change cannot alter the payload or its request hash. Rows without submitter snapshots retain null username/email.

The event deliberately omits admin notes, reviewer fields, revision and update metadata, roles, plan, and unrelated account content. This narrow delivery exception applies only to the exact feedback the user approved after an agent showed the proposed summary and details and asked first. It does not grant playbook runtime general admin roles or general access to user data. Notification copies already delivered outside Kody cannot be recalled and may remain after Kody account deletion under the deployment operator's retention and deletion controls. Such copies contain only the exact approved feedback and attribution, never unrelated account content.

The feedback row is durable before Kody awaits the small Queue enqueue. Enqueue failure is logged but does not change the successful MCP response, avoiding a duplicate submission when a client retries. Queue bodies remain opaque { feedbackId } messages. After admin subscribers are discovered, lazy parameter construction reloads the feedback immediately before any invocation. If deletion removed the row, dispatch throws a typed permanent cancellation and the Queue consumer acknowledges it without invoking or retrying. Other lookup, discovery, or playbook-invocation wrapper infrastructure failures retry before eventually routing exhausted messages to the DLQ. The same idempotency key makes redelivery safe, but a stored failed invocation replays rather than automatically rerunning; the DLQ is the recovery surface. Terminal handler execution failures are isolated without preventing attempts for sibling subscribers.

run.error.recorded

When a user-scoped Activity / run record finishes with status: 'error', Kody dispatches run.error.recorded to playbooks saved by that same user that declare the topic. Delivery is best-effort after a successful run-record Durable Object write — there is no Queue / DLQ for this topic. Failures during subscriber discovery or playbook-invocation infrastructure are logged and do not fail the observed run.

Handlers receive a metadata-first payload:

type RunErrorRecordedEvent = {
	event: 'run.error.recorded'
	run: {
		id: string
		surface: string
		name: string | null
		playbook_id: string | null
		kody_id: string | null
		source_id: string | null
		published_commit: string | null
		storage_id: string | null
		job_id: string | null
		workflow_id: string | null
		invocation_id: string | null
		session_id: string | null
		parent_run_id: string | null
		started_at: string
		finished_at: string | null
		duration_ms: number | null
		error_name: string | null
		error_message: string | null
	}
	activity_url: string
}

activity_url is built from the trusted deployment origin and links to /account/activity/<runId>. The event deliberately omits log lines and the full run metadata blob — fetch detail with runGet when needed. Error name and message use the same truncation budget as the stored run record.

Recursion guard: runs whose surface is subscription never emit this event. Subscription-handler failures themselves create run records; emitting again would recurse. Successful runs and execute successes (which are not persisted) never emit. Failed execute calls do persist and do emit.

Use this topic for notifier playbooks that email, write to Sheets, spawn an agent, or otherwise react when something in the user's account fails.

integration.auth.failed

When host-side OAuth token refresh fails with reconnectable caller state — missing refresh token, provider HTTP 4xx / invalid_grant, missing secrets, host-approval gaps, or invalid connection config — Kody dispatches integration.auth.failed to playbooks saved by that same user that declare the topic. Every classified attempt emits. The platform does not coalesce repeats; notifier playbooks decide how often to ping, typically by pairing this topic with integration.auth.succeeded and storing last-known health in playbook storage. Provider HTTP 5xx and missing connections do not emit.

Delivery is best-effort after the refresh caller error is classified — there is no Queue / DLQ for this topic. Failures during subscriber discovery or playbook-invocation infrastructure are logged and do not change the refresh error the caller sees.

Handlers receive a metadata-first payload:

type IntegrationAuthFailedEvent = {
	event: 'integration.auth.failed'
	event_id: string
	integration: {
		name: string
		lane: 'user' | 'platform'
		account_label: string | null
		description: string | null
		provider: string | null
		platform_app_slug: string | null
		scopes: Array<string>
		connected_at: string | null
		token_refreshed_at: string | null
	}
	reason:
		| 'missing_refresh_token'
		| 'provider_rejected'
		| 'missing_secret'
		| 'host_not_approved'
		| 'invalid_config'
	provider: {
		error: string | null
		error_description: string | null
		http_status: number | null
	}
	reconnect_url: string
	account_url: string
	occurred_at: string
}

reconnect_url is built from the trusted deployment origin and links to /connect/oauth?provider=<name>. When account_label looks like an email it also adds loginHint so Google/OIDC can preselect that account. account_url is the connection detail page (/account/integrations/<name>). The event deliberately omits token values, secret values, client secrets, and secret names. A short-lived access token that refreshes cleanly never emits. Successful Google refreshes persist userinfo.email onto an empty account_label so later reconnect pings can name the account.

Use this topic for notifier playbooks that post to Discord, email, or otherwise ask the owner to reconnect a dead grant.

integration.auth.succeeded

When host-side OAuth token refresh persists a new access token, or /connect/oauth finishes saving tokens for a connection, Kody dispatches integration.auth.succeeded to playbooks saved by that same user that declare the topic. Every successful refresh and every successful connect persist emits. Sequential attempts are not coalesced; concurrent in-flight refreshes of the same connection share one attempt. The platform does not track working ↔ failed itself; notifier playbooks store that edge in playbook storage so a later failure can notify only on the working → failed transition.

Delivery is best-effort after the tokens are written. Failures during subscriber discovery or playbook-invocation infrastructure are logged and do not change the refresh result or the connect response.

Handlers receive a metadata-first payload:

type IntegrationAuthSucceededEvent = {
	event: 'integration.auth.succeeded'
	event_id: string
	integration: {
		name: string
		lane: 'user' | 'platform'
		account_label: string | null
		description: string | null
		provider: string | null
		platform_app_slug: string | null
		scopes: Array<string>
		connected_at: string | null
		token_refreshed_at: string | null
	}
	source: 'refresh' | 'oauth_connect'
	account_url: string
	occurred_at: string
}

source is refresh for refreshIntegrationTokens and oauth_connect for the /connect/oauth persist path. account_url is built from the trusted deployment origin and links to /account/integrations/<name>. The event deliberately omits token values, secret values, client secrets, and secret names.

Use this topic with integration.auth.failed to flip stored health back to working after a reconnect, or to send an all-clear.

mcp.server.disconnected / mcp.server.reconnected

When a saved, enabled outbound MCP server leaves ready and stays unavailable after the hub's lightweight reconnect (two connectToServer + discover attempts, no OAuth restart), Kody dispatches mcp.server.disconnected to playbooks saved by that same user that declare the topic. When that down episode later observes ready again, Kody dispatches mcp.server.reconnected with the same server.episode_id.

Never-ready servers (still authenticating after add), disabled servers, and in-flight connecting / connected / discovering states do not emit. Token loss that parks in authenticating after a prior ready emits disconnected without the lightweight retry — the user must reopen /account/mcp-servers. mcpServerReconnect remains the explicit OAuth restart; listener playbooks should not call it on every event.

Delivery is best-effort after the hub observes the transition — there is no Queue / DLQ for these topics. Failures during subscriber discovery or playbook-invocation infrastructure are logged and do not fail the MCP tool call or snapshot that noticed the change.

Handlers receive a metadata-first payload:

type McpServerConnectionEvent = {
	event: 'mcp.server.disconnected' | 'mcp.server.reconnected'
	event_id: string
	server: {
		id: string
		name: string
		state: string
		previous_state: string
		episode_id: string
	}
	observed_at: string
	account_url: string
}

account_url is built from the trusted deployment origin and links to /account/mcp-servers/<id>. The event omits server URLs, OAuth tokens, bearer headers, auth URLs, and discovered tool lists. Fetch live status with mcpServerList when needed. Idempotency keys include the topic, episode id, and subscriber playbook id, so one disconnected and one reconnected invoke per episode.

Use these topics for notifier playbooks that post to Discord or otherwise tell the owner an MCP server (for example home) dropped or came back. Do not scrape run-error strings for connection health.

repo.pushed

Retired product topic. Personal playbooks have no repository, push, or activation lane. Use playbookSave for complete source and playbookGet for files plus the edit token; do not build automations around Git lifecycle events.

repo.created / repo.deleted

Retired repository product topics. Personal source creation and deletion use the playbook lifecycle, not repository capabilities.

playbook.codemod.applied

Legacy Git-source migration guidance does not apply to personal source. Read and edit current files with the compare-and-set token. Do not assume a commit-based migration or historical revision exists.

playbook.codemod.reverted

Personal playbooks retain no source history or restore snapshots. Correct code by saving a deliberate edit with the current token.

community.activity.recorded (admins)

Retired catalog topic. Personal playbooks have no community activity feed.

community.listing.published (admins)

Retired catalog topic. Personal playbooks are private and have no publication step. Use the current save/edit/run lifecycle instead.

status.incident.opened / status.incident.resolved (admins)

When the isolated status worker opens or resolves a component incident, it best-effort POSTs a metadata-only payload to the main worker (POST /__maintenance/status-incidents, shared bearer STATUS_INCIDENT_EVENT_SECRET). The main worker fans out immediately to playbooks saved by users who hold the admin role at dispatch time. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next incident.

There is no Queue / DLQ for these topics. A missing secret, a down main worker, or a failed invoke is logged and skipped. Playbooks that also reconcile https://status.kody.codes/status.json can catch an incident that is still open, or still listed in recent history, on the next sweep. An incident that opens and resolves between polls can be missed. Probe recording never waits on fan-out.

Handlers receive operator telemetry only:

type StatusIncidentOpenedEvent = {
	event: 'status.incident.opened'
	status_url: string
	incident: {
		component: string
		detail: string | null
		started_at: string
	}
}

type StatusIncidentResolvedEvent = {
	event: 'status.incident.resolved'
	status_url: string
	incident: {
		component: string
		detail: string | null
		started_at: string
		resolved_at: string
	}
}

status_url is the public status page (https://status.kody.codes). component is a status-page card id such as app_db or app. detail is the probe reason (timeout, error, …) or null. Timestamps are ISO-8601 UTC. The event omits probe logs, health-check bodies, user identities, secrets, and unrelated account content. Idempotency keys include the topic, component, timestamps, and playbook id so a retried POST does not double-invoke.

fleet.playbook_error_rate.elevated (admins)

The hourly usage_aggregation lane queries Analytics Engine for anonymous fleet totals of playbook_export, playbook_static_call, job_run, and workflow_run. It compares the last completed hour to the hour before it, and the last 24 hours to the 24 hours before that. When the combined error rate rises past a volume floor, Kody writes a KV snapshot for /admin/insights and fans fleet.playbook_error_rate.elevated to playbooks saved by users who hold the admin role at dispatch time. A second query then groups recent-window errors by owner. One account at ≥80% of those errors, or three accounts together at ≥80%, is concentrated; a true multi-user spike stays fleet-wide. Concentrated pages still fan out to admin playbooks — they name the owning accounts instead of looking like a fleet outage. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next elevation.

There is no Queue / DLQ for this topic. A missed invoke is logged and does not fail usage rollup aggregation. A six-hour cooldown suppresses repeat pages during a prolonged incident.

Handlers receive operator telemetry only:

type FleetPlaybookErrorRateElevatedEvent = {
	event: 'fleet.playbook_error_rate.elevated'
	event_id: string
	status_url: string
	insights_url: string
	environment: string
	observed_at: string
	trigger: {
		window: 'hour' | 'day'
		reason: 'absolute_delta' | 'relative_factor' | 'from_zero'
		recent: {
			start: string
			end: string
			combined: { events: number; errors: number; rate: number | null }
			by_metric: Array<{
				metric:
					| 'playbook_export'
					| 'playbook_static_call'
					| 'job_run'
					| 'workflow_run'
				events: number
				errors: number
				rate: number | null
			}>
		}
		previous: {
			start: string
			end: string
			combined: { events: number; errors: number; rate: number | null }
			by_metric: Array<{
				metric:
					| 'playbook_export'
					| 'playbook_static_call'
					| 'job_run'
					| 'workflow_run'
				events: number
				errors: number
				rate: number | null
			}>
		}
	}
	by_metric: Array<{
		metric:
			'playbook_export' | 'playbook_static_call' | 'job_run' | 'workflow_run'
		events: number
		errors: number
		rate: number | null
	}>
	concentration: {
		kind: 'one_account' | 'few_accounts' | 'fleet'
		recent_errors: number
		owner_count: number
		playbook_count: number
		top_owner_share: number
		owners: Array<{
			username: string
			error_share: number
			playbooks: Array<{ kody_id: string }>
		}>
	} | null
}

status_url is the public status page. insights_url is the operator insights dashboard. Counts are fleet-wide and weighted by Analytics Engine _sample_interval. concentration is present when the elevation query succeeds. owners is populated only for one_account and few_accounts after D1 resolves usernames and playbook kody ids. The event omits user ids, playbook UUIDs, emails, error strings, logs, and unrelated account content. Idempotency keys include the topic, event id, and subscriber playbook id.

Use this topic for notifier playbooks that enqueue a Kody-repo investigation request. Agent spawning stays on the scheduled sweep, not in the subscription handler. Do not treat this topic as permission to read another user's Activity or playbook source.

fleet.entitlement.crossed (admins)

The hourly usage_entitlement_alert lane sweeps the top ~15 active accounts this UTC month and fans fleet.entitlement.crossed to playbooks saved by users who hold the admin role at dispatch time. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next crossing.

One event fires per crossing of 80% (approaching) or 100% (reached) on a specific entitlement, when a non-admin account first exceeds 24h of combined execute / job / workflow runtime in the UTC month, when a non-admin account first reaches a plan-aware unique Dynamic Worker cost threshold this UTC month (Free $2, Standard $12, Pro $49; max and admin accounts do not page), or when a non-admin account first hits 100% of execute_calls_per_day on three of the last seven UTC days. Staying over the same threshold does not emit again. A later drop below that threshold, then a climb back over it, is a new instance. A same-hour jump to 100% emits reached only and claims the 80% crossing so a later drop into the 80–99% band stays silent. Execute-cap days are recorded on durable hit keys so a later drop below 100% the same day does not erase the train.

There is no Queue / DLQ for this topic. A missed invoke is logged and does not fail the hourly sweep. Retry happens on the next hour if the crossing is still unclaimed.

Handlers receive operator telemetry only:

type FleetEntitlementCrossedEvent =
	| {
			event: 'fleet.entitlement.crossed'
			kind: 'entitlement'
			user: { id: string; username: string }
			resource:
				| 'saved_playbooks'
				| 'scheduled_jobs'
				| 'repo_sessions'
				| 'email_sends_per_day'
				| 'email_receives_per_day'
				| 'stored_email_messages'
				| 'secrets'
				| 'concurrent_workflows'
				| 'storage_bytes'
				| 'execute_calls_per_day'
				| 'outbound_fetches_per_day'
				| 'job_runs_per_day'
			label: string
			threshold: 'approaching' | 'reached'
			current: number
			limit: number
			percent_of_limit: number
			insights_url: string
			users_url: string
			observed_at: string
	  }
	| {
			event: 'fleet.entitlement.crossed'
			kind: 'runtime_duration'
			user: { id: string; username: string }
			total_duration_ms: number
			threshold_ms: number
			insights_url: string
			users_url: string
			observed_at: string
	  }
	| {
			event: 'fleet.entitlement.crossed'
			kind: 'repeated_entitlement'
			user: { id: string; username: string }
			resource: 'execute_calls_per_day'
			days_at_limit: number
			window_days: 7
			threshold_days: 3
			insights_url: string
			users_url: string
			observed_at: string
	  }
	| {
			event: 'fleet.entitlement.crossed'
			kind: 'dynamic_worker_cost'
			user: { id: string; username: string }
			unique_worker_days: number
			estimated_gross_usd: number
			threshold_usd: number
			insights_url: string
			users_url: string
			observed_at: string
	  }

user.id is the stable account user id. insights_url and users_url are operator dashboards. Timestamps are ISO-8601 UTC. The event omits emails, plan names, secrets, playbook source, and unrelated account content. Idempotency keys include the topic, user id, crossing kind, threshold or UTC month, resource, UTC day for *_per_day resources and repeated_entitlement, and subscriber playbook id.

Use this topic for notifier playbooks that send an operator message (for example Discord) when an account first crosses a plan limit, repeats an execute cap, or crosses the unique-worker cost line. Filter on kind if a busy-day 80% crossing is too noisy. Do not treat this topic as permission to read another user's playbooks, secrets, or Activity.

User created and deleted (admins)

Password signup, social-login signup, and admin-created person accounts dispatch user.created after the account row and default user role exist. Self-service account deletion at /account dispatches user.deleted after the per-user cascade finishes. Platform accounts (reserved official playbook owners) do not emit user.created.

Production fan-out selects only playbooks whose owners hold the admin role at dispatch time. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next create or delete.

There is no Queue / DLQ for these topics. Dispatch is best-effort after the account change commits: a failed invoke is logged and does not fail signup, admin create, or account deletion.

Handlers receive a metadata-only identity snapshot:

type UserCreatedEvent = {
	event: 'user.created'
	user: {
		id: string
		username: string
		email: string
	}
	source: 'signup' | 'oauth' | 'admin'
	created_at: string
	invite_code: string | null
	attribution: {
		utm_source: string | null
		utm_medium: string | null
		utm_campaign: string | null
		utm_content: string | null
		utm_term: string | null
		landing_path: string | null
		referrer: string | null
	}
}

type UserDeletedEvent = {
	event: 'user.deleted'
	user: {
		id: string
		username: string
		email: string
	}
	deleted_at: string
}

user.id is the stable account user id. source is the create path that committed. invite_code is the consumed, normalized invite code when signup used one, otherwise null. attribution is first-touch marketing UTMs and landing path/referrer persisted on the account at signup (all null when absent). Timestamps are ISO-8601 UTC. The event omits passwords, roles, plan, secrets, playbooks, and unrelated account content. Notification copies already delivered outside Kody cannot be recalled after account deletion. Idempotency keys include the topic, user id, timestamp, and playbook id.

user.email_verification.failed (admins)

The first terminal Cloudflare lifecycle event on a signup/verify send (bounced, failed, rejected, or complained) fans user.email_verification.failed to playbooks saved by users who hold the admin role at dispatch time. A later replay of the same terminal state does not emit again. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next failure.

There is no Queue / DLQ for this topic. Dispatch is best-effort after the user row already carries the bounce: a failed invoke is logged and does not fail delivery-event processing.

Handlers receive a metadata-only operator snapshot:

type UserEmailVerificationFailedEvent = {
	event: 'user.email_verification.failed'
	user: {
		id: string
		username: string
		email: string
	}
	status: 'bounced' | 'failed' | 'rejected' | 'complained'
	class: 'sender_block' | 'other' | null
	admin_user_url: string
	occurred_at: string
}

user.id is the stable account user id. class is sender_block for Fastmail-style domain/IP blocks (RLR613, RLR813, blacklist language), other for generic terminal failures, or null when the event is not classified. admin_user_url is the operator page for that account. Timestamps are ISO-8601 UTC. The event omits SMTP transcripts, verification tokens, passwords, roles, plan, secrets, and unrelated account content. Idempotency keys include the topic, user id, timestamp, and subscriber playbook id.

Use this topic for notifier playbooks that email or page an operator when signup/verify mail bounces or otherwise fails at the provider. Silent drops that stay accepted use user.email_verification.stalled. user.created still fires for every new person account, including accounts that later verify themselves. Do not treat this topic as permission to mark the account verified or mint a link — call adminUserVerify from an admin session when ownership is proven.

user.email_verification.stalled (admins)

The hourly email_verification_stall_alert lane lists unverified person accounts whose latest signup/verify send is still accepted after 60 minutes with no Cloudflare lifecycle event (delivered, bounced, failed, rejected, or complained). Each matching send fans user.email_verification.stalled to playbooks saved by users who hold the admin role at dispatch time. The scan walks that derived set in pages of 50 using a KV watermark so later sends are not starved behind the oldest unresolved rows. A later hourly scan of the same accepted timestamp does not emit again. A resend that stamps a new accepted time can emit again after another hour. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next scan.

There is no Queue / DLQ for this topic. Dispatch is best-effort after the user row already carries accepted: a failed invoke is logged and does not fail the hourly cron.

Handlers receive a metadata-only operator snapshot:

type UserEmailVerificationStalledEvent = {
	event: 'user.email_verification.stalled'
	user: {
		id: string
		username: string
		email: string
	}
	status: 'accepted'
	accepted_at: string
	stall_after_minutes: number
	admin_user_url: string
	occurred_at: string
}

user.id is the stable account user id. accepted_at is users.email_verification_delivery_at for that send. stall_after_minutes is the scan threshold (60). occurred_at is the scan time. admin_user_url is the operator page for that account. Timestamps are ISO-8601 UTC. The event omits SMTP transcripts, verification tokens, passwords, roles, plan, secrets, and unrelated account content. Idempotency keys include the topic, user id, accepted timestamp, and subscriber playbook id.

Use this topic for notifier playbooks that email or page an operator when a signup is stranded without a bounce. SimpleLogin-style aliases can drop kody@ mail without a terminal Cloudflare event. Terminal failures still use user.email_verification.failed. /admin/users and adminUserList accept verification=stalled for the same derived set. Do not treat this topic as permission to mark the account verified or mint a link — call adminUserVerify from an admin session when ownership is proven.

user.email_outbound.paused (admins)

The delivery-queue abuse lane pauses outbound sending after one spam complaint or five bounced sends in a UTC day, then fans user.email_outbound.paused to playbooks saved by users who hold the admin role at dispatch time. A later replay of the same pause write does not emit again. A non-admin playbook may declare the topic, but it never receives the event. Role revocation stops delivery on the next pause.

There is no Queue / DLQ for this topic. Dispatch is best-effort after the pause is already committed: a failed invoke is logged and does not fail delivery-event processing.

Handlers receive a metadata-only operator snapshot:

type UserEmailOutboundPausedEvent = {
	event: 'user.email_outbound.paused'
	user: {
		id: string
		username: string
		email: string
	}
	reason: 'complained' | 'bounced'
	bounce_threshold: number | null
	admin_user_url: string
	occurred_at: string
}

user.id is the stable account user id. bounce_threshold is the daily bounce count that triggered the pause (5) when reason is bounced, otherwise null. admin_user_url is the operator page for that account. Timestamps are ISO-8601 UTC. The event omits SMTP transcripts, message bodies, passwords, roles, plan, secrets, and unrelated account content. Idempotency keys include the topic, user id, timestamp, and subscriber playbook id.

Use this topic for notifier playbooks that email or page an operator when one account's outbound sending is paused. Do not treat this topic as permission to clear the pause — call the audited resume_email_outbound admin action after review. Shared-domain pressure uses email.delivery.burst.

auth.denial.burst (admins)

The hourly auth_denial_alert lane counts MCP auth failures (mcp_token_rejected, mcp_capability_denied) in the last 60 minutes. When the count crosses 50, it fans auth.denial.burst to playbooks saved by users who hold the admin role at dispatch time. A six-hour KV cooldown suppresses repeat pages on the same sustained spike. A non-admin playbook may declare the topic, but it never receives the event.

There is no Queue / DLQ for this topic. A missed invoke is logged and does not fail the hourly cron. Audit rows and /admin/insights remain the browse surface.

Handlers receive operator telemetry only:

type AuthDenialBurstEvent = {
	event: 'auth.denial.burst'
	count: number
	threshold: number
	window_minutes: number
	insights_url: string
	observed_at: string
}

insights_url is the operator insights dashboard. The event omits user ids, token ids, capability names, request bodies, and unrelated account content. Idempotency keys include the topic, observed timestamp, and subscriber playbook id.

Use this topic for notifier playbooks that page an operator when permission probing or a compromised account is likely. Do not treat this topic as permission to suspend an account.

email.delivery.burst (admins)

The hourly email_delivery_alert lane counts platform-wide Cloudflare Email Sending outcomes of complained or bounced in the last 60 minutes. When the count crosses 20, it fans email.delivery.burst to playbooks saved by users who hold the admin role at dispatch time. A six-hour KV cooldown suppresses repeat pages. A non-admin playbook may declare the topic, but it never receives the event.

There is no Queue / DLQ for this topic. A missed invoke is logged and does not fail the hourly cron. Thin email_delivery_alert_events rows and the Email delivery health chart on /admin/insights remain the browse surface.

Handlers receive operator telemetry only:

type EmailDeliveryBurstEvent = {
	event: 'email.delivery.burst'
	count: number
	threshold: number
	window_minutes: number
	insights_url: string
	observed_at: string
}

insights_url is the operator insights dashboard. The event omits user ids, recipients, message bodies, SMTP transcripts, and unrelated account content. Idempotency keys include the topic, observed timestamp, and subscriber playbook id.

Use this topic for notifier playbooks that page an operator when the shared sending domain is under platform-wide pressure. The per-user user.email_outbound.paused topic still fires when one account is paused.

Working with an agent? This guide is also plain markdown at /guides/playbook-subscriptions.md, or load it over MCP with search({ entity: 'playbook_subscriptions:guide' }).