一键导入
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 职业分类
Adding or changing routes in `apps/api`. One source of truth (`defineApiEndpoint` + a Zod schema) becomes an HTTP endpoint, an OpenAPI operation, an MCP tool, and a TS SDK method — descriptions and contracts must be written with all four readers in mind.
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.
Preparing a production release, pushing a vX.Y.Z release tag, running scripts/release.sh, or updating CHANGELOG.md with the changes that are about to be deployed to production.
Enables or disables Latitude production maintenance mode by redirecting all publicly exposed production services to the Better Stack status page. Use when asked to start, stop, toggle, verify, or prepare a maintenance window.
Adding or reading env vars, updating .env.example, or validating config at startup with parseEnv / parseEnvOptional.
ClickHouse queries, Goose migrations, chdb test schema, 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.getIssueAlignmentState sees status === "running"). Guard the change with patched("<descriptive-id>") from @temporalio/workflow, or pin the new code to a new Worker Deployment Version, before merging. See temporal-developer and its references/typescript/versioning.md for the three-step patched → deprecatePatch → remove flow and Worker Versioning setup. Draining in-flight workflows before deploy is rarely viable here: optimizeEvaluationWorkflow activities are sized up to 75 min (GEPA budget) and the full pipeline can run substantially longer, so a deploy window almost never finds all evaluations:* workflow IDs idle.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 TracesIngested. The batch event fans out one publish per deduped trace id; every new publish for the same trace resets the clock, so end-of-trace work fires once that 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.