Skip to content
DocsSubscriptions and events

Packages

Package subscriptions and events

Official Kody doc

Use package subscriptions when a saved package should react to Kody-owned event topics. The saved package remains the top-level entity; subscriptions are nested manifest metadata and package 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): package-local module path for the event handler.
  • description (optional): human-readable purpose for package detail and subscription listings.
  • filters (optional): topic-specific metadata reserved for dispatchers.

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

Discovery

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

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

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

Synthetic dispatch

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

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

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

{
	"kody_id": "email-automation",
	"package_scope": "kody",
	"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.

Final published and already_published results from packagePublishExternalPush include test_hints.subscriptions[] with a starter snippet per declared topic when subscriptions are present. For a dispatched result, poll the workflow to completion before reading its final publish result. Failed and non-fast-forward results have no test hints.

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 package-emitted topics skips subscribers when filters do not match the payload; synthetic dispatch always runs the named package. Put filter-matching fields inside params when testing filter-dependent code paths.
  • Admin-only topics (email.system-message.received, platform.feedback.submitted, community.activity.recorded, community.listing.published, status.incident.opened, fleet.package_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.

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

Packages can define their own event topics and emit to them; every other package 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 package'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 package checks at publish time so authors never rely on silently ignored constraints. Declared schemas appear in package search/detail projections so subscribers can discover payload shapes.

Emitting

Emit from any package runtime context (exports, subscription handlers, package-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 package'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 packageStorage() and emit a reference instead.
  • events.dispatch is unavailable in ad hoc execute runs — topics belong to packages, so emit from package code (or statically import a package export that dispatches).

Delivery semantics

Dispatch is asynchronous and durable: events.dispatch validates the event, enqueues it on the kody-package-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 packages at delivery time and invokes each subscription:@scope/topic handler with:

type PackageEventEnvelope = {
	event: string
	source: { type: 'package'; package_id: string; kody_id: string }
	idempotency_key: string
	payload: Record<string, unknown>
}
  • Per-subscriber invocations are exactly-once keyed on (source package, subscriber package, 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-package-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 packages 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 package-emitted topics

A subscription to a package-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 package 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 package 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 packages 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 package 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 packages 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 package 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 packages. 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 package id. Deduplicate in package 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 packages saved by users who hold the admin role when the message is processed. A non-admin package 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 package 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 package-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 packages 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 package-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
		package_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 packages 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 packages saved by that same user that declare the topic. Every classified attempt emits. The platform does not coalesce repeats; notifier packages decide how often to ping, typically by pairing this topic with integration.auth.succeeded and storing last-known health in package 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 package-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 packages 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 packages 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 packages store that edge in package 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 package-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 packages 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 packages 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 package-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 package id, so one disconnected and one reconnected invoke per episode.

Use these topics for notifier packages 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

When Cloudflare Artifacts reports commits pushed to a Kody-managed Artifacts repo (plain repo, package, or job source), Kody dispatches repo.pushed to packages saved by that same user that declare the topic. Delivery is durable via the kody-artifacts-repo-events Queue (with DLQ). Session fork repos, session workspace branch pushes (sessions/<id>), and publish git-notes (refs/notes/commits) never emit. Opening a repo session git-pushes the session ref, and delivering it as repo.pushed would let a handler that opens another session loop. Publish attaches a metadata note after the source-branch push; that second git update is not a content push. Events for other ARTIFACTS_NAMESPACE values are ignored. Handlers that only care about default branch content should still check push.ref.

Handlers receive a metadata-first payload:

type RepoPushedEvent = {
	event: 'repo.pushed'
	repo: {
		source_id: string
		repo_id: string
		entity_kind: 'repo' | 'package' | 'job'
		entity_id: string
		name: string | null
		kody_id: string | null
	}
	push: {
		ref: string
		before: string
		after: string
		total_commits_count: number
		commits_truncated: boolean
		commits: Array<{
			id: string
			message: string
			message_truncated: boolean
			timestamp: string
			author: { name: string; email: string }
			committer: { name: string; email: string }
			parents: Array<string>
		}>
	}
	artifacts: {
		namespace: string
		event_timestamp: string
		event_subscription_id: string
	}
}

repo_id is the Artifacts repo name (also stored on entity_sources.repo_id). name is the user-facing plain-repo name or package npm name when known; kody_id is set for packages. For entity_kind: 'package' | 'job', a push updates live HEAD but does not mean the package/job published commit advanced — use publish / external-push / reconcile for activation.

Idempotency keys include the after commit, ref, and subscriber package id, so Queue redelivery is safe.

repo.created / repo.deleted

Account-level Artifacts create/delete events map to repo.created and repo.deleted with the same repo entity block plus Artifacts metadata (default_branch, description, Cloudflare cloudflare_repo_id). Same-user fan-out and Queue delivery match repo.pushed. Unmatched deletes (D1 row already gone) are acknowledged without retry.

package.codemod.applied

After a successful package codemod apply, Kody dispatches package.codemod.applied to packages saved by the owning user of the migrated package that declare the topic. Delivery follows the same best-effort host dispatch path as run.error.recorded — there is no Queue / DLQ for this topic. Failures during subscriber discovery or package-invocation infrastructure are logged and do not fail the codemod apply.

Handlers receive a metadata-first payload:

type PackageCodemodSubscriptionEnvelope = {
	event: 'package.codemod.applied'
	codemod: {
		id: string
		description: string
	}
	package: {
		package_id: string
		kody_id: string
	}
	run: {
		run_id: string
		item_id: string
	}
	changed_paths: Array<string>
	before_commit: string | null
	after_commit: string | null
}

changed_paths lists published-tree paths the codemod transform modified. before_commit and after_commit are the package's published commit before and after apply. The event deliberately omits file contents — fetch the current published source with repo or package capabilities when a handler needs diffs or full files. Community listing snapshots are unchanged by apply; only the owning saved package advances. run.item_id is the apply ledger item id.

Use this topic for notifier packages that record migrations, ping owners, or trigger follow-up automation when platform codemods rewrite user package source.

package.codemod.reverted

After a successful package codemod revert, Kody dispatches package.codemod.reverted to packages saved by the owning user of the restored package that declare the topic. Delivery semantics match package.codemod.applied and run.error.recorded.

Handlers receive the same envelope shape with event: 'package.codemod.reverted':

type PackageCodemodSubscriptionEnvelope = {
	event: 'package.codemod.reverted'
	codemod: {
		id: string
		description: string
	}
	package: {
		package_id: string
		kody_id: string
	}
	run: {
		run_id: string
		item_id: string
	}
	changed_paths: Array<string>
	before_commit: string | null
	after_commit: string | null
}

For revert, before_commit is the post-codemod published commit (the source apply item's afterCommit) and after_commit is the restored pre-codemod commit. changed_paths is copied from the source apply item (paths the codemod originally changed), not recomputed at revert time. run.item_id is the new revert-run ledger item id. Revert snapshots expire from KV after 90 days, so revert and this event are unavailable once the snapshot is gone.

Use this topic when automation must react to an operator or user undoing a prior codemod apply.

community.activity.recorded (admins)

Successful community fork and rating writes enqueue a durable community.activity.recorded attempt. The Queue consumer dispatches only to packages saved by users who hold the admin role when the message is processed. Non-admin declarations are inert, and role revocation applies to the next attempt.

Handlers receive activity metadata only:

type CommunityActivityRecordedEvent = {
	event: 'community.activity.recorded'
	event_id: string
	activity:
		| {
				id: string
				kind: 'fork'
				listing: { id: string; name: string; kody_id: string }
				actor: { username: string | null }
				occurred_at: string
		  }
		| {
				id: string
				kind: 'rating'
				listing: { id: string; name: string; kody_id: string }
				actor: { username: string | null }
				occurred_at: string
				stars: number
				adaptation_effort: number
		  }
}

The event omits stable user ids, email, forked package/source ids, target kody ids, rating notes, package source, secrets, and unrelated account content. One-click installs and ordinary forks both appear as fork because they share the same existing community_forks row shape. Rating records are upserts, so the reloaded activity contains the latest scores.

Queue messages contain only { eventId, kind, activityId }. Dispatch reloads the metadata projection after admin subscriber discovery. Missing activity is a permanent cancellation; transient lookup, discovery, and package-invocation infrastructure failures retry and can reach the dedicated DLQ. event_id provides a distinct package-invocation idempotency key for every recorded write.

community.listing.published (admins)

The first successful community listing publish enqueues a durable community.listing.published attempt. Republishes record listing_updated in the activity timeline but do not enqueue this subscription topic. The Queue consumer dispatches only to packages saved by users who hold the admin role when the message is processed. Non-admin declarations are inert, and role revocation applies to the next attempt.

Handlers receive listing metadata only:

type CommunityListingPublishedEvent = {
	event: 'community.listing.published'
	event_id: string
	listing: {
		id: string
		name: string
		kody_id: string
		description: string | null
		public_url: string
	}
	publisher: {
		username: string | null
	}
	published_at: string
}

public_url is the canonical shareable URL ({base}/@{username}/{kody_id}), never /community/{listing_id}. The event omits stable user ids, email, package source, secrets, and unrelated account content.

Queue messages contain only { eventId, listingId }. Dispatch reloads the metadata projection after admin subscriber discovery. Missing, delisted, or unpublished listings are a permanent cancellation; transient lookup, discovery, and package-invocation infrastructure failures retry and can reach the dedicated DLQ. event_id provides a distinct package-invocation idempotency key for every first-publish enqueue. Enqueue failures are logged and never fail communityPublish.

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 packages saved by users who hold the admin role at dispatch time. A non-admin package 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. Packages 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 package id so a retried POST does not double-invoke.

fleet.package_error_rate.elevated (admins)

The hourly usage_aggregation lane queries Analytics Engine for anonymous fleet totals of package_export, package_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.package_error_rate.elevated to packages 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 packages — they name the owning accounts instead of looking like a fleet outage. A non-admin package 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 FleetPackageErrorRateElevatedEvent = {
	event: 'fleet.package_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:
					'package_export' | 'package_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:
					'package_export' | 'package_static_call' | 'job_run' | 'workflow_run'
				events: number
				errors: number
				rate: number | null
			}>
		}
	}
	by_metric: Array<{
		metric:
			'package_export' | 'package_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
		package_count: number
		top_owner_share: number
		owners: Array<{
			username: string
			error_share: number
			packages: 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 package kody ids. The event omits user ids, package UUIDs, emails, error strings, logs, and unrelated account content. Idempotency keys include the topic, event id, and subscriber package id.

Use this topic for notifier packages 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 package 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 packages saved by users who hold the admin role at dispatch time. A non-admin package 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_packages'
				| '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, package 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 package id.

Use this topic for notifier packages 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 packages, 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 package owners) do not emit user.created.

Production fan-out selects only packages whose owners hold the admin role at dispatch time. A non-admin package 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, packages, 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 package 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 packages 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 package 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 package id.

Use this topic for notifier packages 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 packages 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 package 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 package id.

Use this topic for notifier packages 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 packages 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 package 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 package id.

Use this topic for notifier packages 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 packages 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 package 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 package id.

Use this topic for notifier packages 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 packages saved by users who hold the admin role at dispatch time. A six-hour KV cooldown suppresses repeat pages. A non-admin package 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 package id.

Use this topic for notifier packages 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 page is also plain markdown at /docs/package-subscriptions.md, or load it over MCP with search({ entity: 'package_subscriptions:guide' }).