一键导入
async-jobs-and-events
Queues and workers, domain event publishers, async notifications or projections, or not doing that work inside HTTP handlers.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Queues and workers, domain event publishers, async notifications or projections, or not doing that work inside HTTP handlers.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Layering and boundaries, web vs public API, app layout (clients, routes, logging), ports/adapters, runtime-portable domain/shared/utils code, multi-tenancy, DDD layout, or anti-patterns.
Review the current conversation context and git changes, then persist durable repository knowledge into `dev-docs/*.md` by domain and into `AGENTS.md` for cross-cutting repo rules. Use after features, fixes, refactors, architecture changes, schema changes, or when the user mentions docs, documentation, design, architecture, business logic, conventions, or `AGENTS.md`.
Biome formatting, import style, strict TypeScript, naming (including React file names), or generated files.
Writing or debugging tests, choosing unit vs integration style, Postgres/ClickHouse tests, regenerating ClickHouse test schema, or exporting test helpers from packages without pulling test code into production bundles.
apps/web UI — routes, @repo/ui, TanStack Start server functions and collections, forms (useForm + createFormSubmitHandler + fieldErrorsAsStrings for Zod field errors), Tailwind layout rules, design-system updates, and useEffect / useMountEffect policy.
ClickHouse queries, Goose migrations, chdb test schema, Weaviate collections/migrations, or telemetry storage paths.
| name | async-jobs-and-events |
| description | Queues and workers, domain event publishers, async notifications or projections, or not doing that work inside HTTP handlers. |
When to use: Queues and workers, domain event publishers, async notifications or projections, or not doing that work inside HTTP handlers.
Domain events represent facts that happened — state transitions on an aggregate — not instructions for what should happen next. The publisher must never know or care which handlers are subscribed.
ScoreCreated, ScoreStatusChanged. Bad: ScoreDraftSaved (named to route around a handler), ScoreReadyForDiscovery (named after a consumer concern).ScoreCreated — regardless of whether the score is a draft or published.issues:discovery:${scoreId}:${status} instead of splitting into separate event types.A new event type is justified when it represents a genuinely distinct state transition that would exist even with zero handlers — for example, ScoreDeleted is a different fact from ScoreCreated. The test: does the aggregate's lifecycle model include this transition independently of downstream concerns?
// BAD — publisher decides routing based on consumer needs
const eventName = score.draftedAt === null ? “ScorePublished” : “ScoreDraftSaved”
yield* outboxEventWriter.write({ eventName, ... })
// GOOD — one canonical event, consumers filter
yield* outboxEventWriter.write({
eventName: “ScoreCreated”,
payload: { scoreId: score.id, organizationId, projectId, status: score.draftedAt === null ? “published” : “draft” },
})
// Consumer side — handler owns its filtering
ScoreCreated: (event) =>
Effect.all([
// discovery uses status-aware dedupe key, skips drafts internally
pub.publish(“issues”, “discovery”, event.payload, {
dedupeKey: `issues:discovery:${event.payload.scoreId}:${event.payload.status}`,
}),
pub.publish(“annotation-scores”, “publishHumanAnnotation”, event.payload, {
debounceMs: SCORE_PUBLICATION_DEBOUNCE,
}),
])
withTracing from @repo/observability in their pipe chain so that Effect spans flow into the OTel pipeline. See effect-and-errors for tracing rules.OutboxEventWriter service (or a plain OutboxEventWriterShape from createOutboxWriter in @platform/db-postgres) instead of inserting outbox rows directly.createEventsPublisher(queuePublisher) into domain-events instead of persisting an outbox row only to forward it.apps/workers, and durable multi-step workflows live in the Temporal-backed apps/workflows app.organizationId and projectId in domain-event payloads, topic/task payloads, and workflow inputs by default. Exceptions: MagicLinkEmailRequested, InvitationEmailRequested, UserDeletionRequested, the domain-events topic payload, the magic-link-email topic payload, the invitation-email topic payload, and the user-deletion topic payload.OutboxEventWriter / OutboxEventWriterShape for transactional boundaries and direct EventsPublisher publication for non-transactional or high-volume worker flows. Downstream side effects should run from the domain-event consumers rather than inline in the delayed task.debounceMs and throttleMsPublishOptions exposes two mutually exclusive delay fields. Both accept a window in ms and coalesce repeated publishes against dedupeKey, but they answer different questions.
debounceMs — fires after N ms of quiet on the dedupe key. Each publish within the window pushes the fire time forward and replaces the pending payload (BullMQ extend: true, replace: true). Use when the task should wait for a stream of events to settle.
Example: trace-end:run after SpanIngested. Every new span for a trace resets the clock; end-of-trace work fires once the trace is actually idle. If spans keep arriving every few seconds, that means the trace is still active — not firing is correct.
throttleMs — fires at most once per N ms per dedupe key. The first publish schedules the fire time; subsequent publishes within the window are dropped (BullMQ extend: false, replace: false). Requires dedupeKey. Use when you need a hard upper bound on fire latency and a cap on frequency, and where starvation under a continuous publish stream would be a product bug.
Example: annotation-driven alignment refresh (evaluations:automaticRefreshAlignment, 1h) and its escalation (evaluations:automaticOptimization, 8h). We want at most one refresh per evaluation per hour, firing at most 1h after the first new annotation, even if annotations keep arriving every 30 min.
Ask: if a publisher fires every 30 min forever on the same dedupeKey, what should happen?
debounceMs. Classic debounce. Fire time keeps sliding forward; fires only during quiet periods.N min regardless" → throttleMs. Bounded latency, bounded frequency; never starves.Reaching for debounceMs when the intent is "run at most once per hour". With a continuous publish stream every publish extends the TTL and the task never fires — silent starvation. If the wording in the spec or PR is "at most once per X" or "every X at most", that is throttle semantics; use throttleMs.
// Debounce — wait for events to settle
pub.publish("trace-end", "run", payload, {
dedupeKey: `trace-end:run:${traceId}`,
debounceMs: TRACE_END_DEBOUNCE_MS,
})
// Throttle — at most once per window, bounded latency
pub.publish("issues", "refresh", payload, {
dedupeKey: `issues:refresh:${issueId}`,
throttleMs: ISSUE_REFRESH_THROTTLE_MS,
})
Name the constant to match the semantic: *_DEBOUNCE_MS vs. *_THROTTLE_MS. A constant named for one semantic that is passed as the other is a lie readers will trip over.
When adding a new external system the product talks to:
packages/platform/*-<provider>.For env var naming when wiring config, see env-configuration. For layer rules, see architecture-boundaries.