--- id: AllocationAdded name: Allocation Added version: 0.0.1 summary: | A partner allocated zones to a grower. Written to events/allocation/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend**; consumed by **Allocation Service** (`allocationAddEvent`). Posts to Slack and calls `notifyGrowersOfAllocation`: loads the allocation (crop, zone count, partner) and, for each grower user, inserts an in-app notification and queues a "New allocation" `UserQueueMessage`. See the [Allocation added](/docs/flows/AllocationAdded) flow. ## Payload (observed) `payload.allocationId`. --- id: DelphiError name: Delphi Error version: 0.0.1 summary: | An error signal from the Delphi backend. Written to events/error/delphi/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** (Delphi); consumed by **Telemetry Notifier** (`delphiErrorEvent`). Posts a red Slack summary — the colour is chosen because the event title contains "error". No downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: DelphiSuccess name: Delphi Success version: 0.0.1 summary: | A success signal from the Delphi backend. Written to events/success/delphi/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** (Delphi); consumed by **Telemetry Notifier** (`delphiSuccessEvent`). Posts a green Slack summary. No downstream fan-out. --- id: FieldAdded name: Field Added version: 0.0.1 summary: | A new field was added for a grower. Written to events/field/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend**; consumed by **Field & Zone Service** (`fieldAddEvent`). Loads the allocation's partner and notifies each partner user — both an in-app `insertNotification` and a `UserQueueMessage` ("New field added"). See the [Field added](/docs/flows/FieldAdded) flow. ## Payload (observed) `growerId`, `allocationId`, `fieldName`. --- id: FieldEdited name: Field Edited version: 0.0.1 summary: | A field was edited. Written to events/field/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a field is edited; consumed by the domain record notifier (`fieldEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: GrowerAdded name: Grower Added version: 0.0.1 summary: | A grower was added. Written to events/grower/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a grower is added; consumed by the domain record notifier (`growerAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: HarvestAdded name: Harvest Added version: 0.0.1 summary: | A harvest was added. Written to events/harvest/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a harvest is added; consumed by the domain record notifier (`harvestAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: HarvestEdited name: Harvest Edited version: 0.0.1 summary: | A harvest was edited. Written to events/harvest/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a harvest is edited; consumed by the domain record notifier (`harvestEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: HermesError name: Hermes Error version: 0.0.1 summary: | An error signal from the Hermes backend. Written to events/error/hermes/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** (Hermes); consumed by **Telemetry Notifier** (`hermesErrorEvent`). Posts a red Slack summary. No downstream fan-out. --- id: LeafIrrigationReceived name: Leaf Irrigation Received version: 0.0.1 summary: | A Leaf field-irrigation alert was received. Written to events/webhook/leafirrigation/{eventId} by the leafIrrigationWebhook endpoint. owners: - platform-team --- ## Overview Produced by the `leafIrrigationWebhook` HTTPS endpoint (signature-verified); consumed by **Leaf Webhook Service** (`leafIrrigationWebhookEvent`). For `newFieldIrrigationActivity` alerts the consumer authenticates to the Leaf API and fetches the irrigation summary. Terminal. See the [Leaf webhook ingress](/docs/flows/LeafWebhookIngress) flow. --- id: LeafOperationReceived name: Leaf Operation Received version: 0.0.1 summary: | A Leaf (withleaf.io) operation alert was received. Written to events/webhook/leafoperation/{eventId} by the leafWebhook HTTP endpoint. owners: - platform-team --- ## Overview Produced by the `leafWebhook` HTTPS endpoint after verifying the `X-Leaf-Signature` HMAC; consumed by **Leaf Webhook Service** (`leafWebhookEvent`). For `operationProcessingFinished` alerts the consumer authenticates to the Leaf API, fetches the operation and its summary, and matches the field in Data Connect. Terminal — no further event. See the [Leaf webhook ingress](/docs/flows/LeafWebhookIngress) flow. --- id: MailgunWebhookReceived name: Mailgun Webhook Received version: 0.0.1 summary: | A Mailgun delivery event (open, bounce, etc.) was received. Written to events/webhook/mailgun/{eventId} by the mailgunWebhook endpoint. owners: - platform-team --- ## Overview Produced by the `mailgunWebhook` HTTPS endpoint after verifying the Mailgun HMAC signature (the full payload is spread into the document); consumed by **Mailgun Webhook Service** (`mailgunWebhookEvent`), which posts a Slack summary. Currently a pass-through for observability. See the [Mailgun webhook ingress](/docs/flows/MailgunWebhookIngress) flow. --- id: PartnerAdded name: Partner Added version: 0.0.1 summary: | A partner was added. Written to events/partner/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a partner is added; consumed by the domain record notifier (`partnerAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: PartnerAllocated name: Partner Allocated version: 0.0.1 summary: | A partner-level allocation signal. Written to events/partner/allocation/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend**; consumed by **Allocation Service** (`partnerAllocationEvent`, built on `createEventHandler`). Slack notifier only — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: PlateAdded name: Plate Added version: 0.0.1 summary: | A plate was added. Written to events/plate/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a plate is added; consumed by the domain record notifier (`plateAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: PlateEdited name: Plate Edited version: 0.0.1 summary: | A plate was edited. Written to events/plate/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a plate is edited; consumed by the domain record notifier (`plateEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: PlatePredictionCompleted name: Plate Prediction Completed version: 0.0.1 summary: | RNA yield predictions for a plate are ready. Written to events/plate/predictioncomplete/{eventId}. owners: - platform-team --- ## Overview Produced by the CropDx backend; consumed by **Plate Processor** (`platePredictionCompleteEvent`). The handler loads the plate with users and fans out three ways: - `ZoneGenerateReport` for each non-dropped zone that matches a hard-coded grower subscription (see `queueZoneReportEvents`); - `SequencePredictionCompleted` (or `SequenceDropped`) per sequence on the plate; - `UserQueueMessage` for users whose samples now have RNA insights. ## Notes for maintainers `queueZoneReportEvents` contains a hard-coded `subscriptions` array keyed by `growerId`. New growers will not receive reports until added there — a good candidate to move into Data Connect later. --- id: PlateScanned name: Plate Scanned version: 0.0.1 summary: | A plate has been scanned in the lab. Written by the CropDx backend to events/plate/scan/{eventId}. owners: - platform-team schemaPath: schema.json --- ## Overview Produced by the CropDx backend; consumed by **Plate Processor** (`plateScanEvent`). On receipt the handler loads the plate with its sequences and users, then: - queues a `SequenceScanned` event for each sample's first sequence, or a `SequenceRepeatScanned` when the sample has been seen before; - notifies users whose sample was received for the first time (via `UserQueueMessage`); - optionally notifies on repeat scans, gated by `SEND_REPEAT_SCAN_NOTIFICATION`. ## Trigger Firestore document create at `events/plate/scan/{eventId}`. ## Notes for maintainers The payload is read as `data.payload.plate_id`. If `plate_id` is missing the handler logs an error and stops — there is no downstream fan-out. ## Raw Schema:schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "PlateScanned", "description": "Document written to events/plate/scan/{eventId}.", "type": "object", "properties": { "payload": { "type": "object", "properties": { "plate_id": { "type": "integer", "description": "ID of the scanned plate in the Data Connect / Postgres database." } }, "required": ["plate_id"] }, "skipSlackNotification": { "type": "boolean", "description": "When true, the consuming handler does not post to Slack. Used for internally-generated fan-out events.", "default": false }, "timestamp": { "type": "string", "format": "date-time", "description": "Firestore serverTimestamp(), or an ISO string in the local emulator." } }, "required": ["payload"] } --- id: PlateSequencingCompleted name: Plate Sequencing Completed version: 0.0.1 summary: | Sequencing for a plate has finished. Written to events/plate/sequencingcomplete/{eventId}. owners: - platform-team --- ## Overview Produced by the CropDx backend; consumed by **Plate Processor** (`plateSequencingCompleteEvent`). The handler posts to Slack and enqueues the `processPullSequencingResultsTask` command, which pulls run statistics from Illumina BaseSpace and then writes `PlateSequencingResultsPulled`. --- id: PlateSequencingResultsPulled name: Plate Sequencing Results Pulled version: 0.0.1 summary: | Run statistics for a plate were pulled from Illumina BaseSpace. Written to events/plate/sequencingresults/{eventId} by the sequencing-results task. owners: - platform-team --- ## Overview Produced by the `processPullSequencingResultsTask` command (not by the external backend); consumed by **Plate Processor** (`plateSequencingResultsPulledEvent`). The handler calls `addPlateMetadata`, which inserts a sequencing-stat row and links it to the matching plate(s) in Data Connect. ## Notes for maintainers This event is written with `skipSlackNotification: true` by the task, but the handler still posts a Slack summary because it has its own `sendSlackNotification` call keyed on `experimentName`. --- id: PurchaseAdded name: Purchase Added version: 0.0.1 summary: | A purchase was added. Written to events/purchase/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a purchase is added; consumed by the domain record notifier (`purchaseAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SampleAdded name: Sample Added version: 0.0.1 summary: | A sample was added. Written to events/sample/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a sample is added; consumed by the domain record notifier (`sampleAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SampleEdited name: Sample Edited version: 0.0.1 summary: | A sample was edited. Written to events/sample/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a sample is edited; consumed by the domain record notifier (`sampleEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SampleProcessDelayed name: Process Delayed Samples version: 0.0.1 summary: | On-demand trigger to run the delayed-sample orchestration. Written to events/sample/processdelayed/{docId}. owners: - platform-team --- ## Overview Consumed by **Delayed Sample Service** (`processDelayedSamplesOnDemand`). Creating a document here runs the same `orchestrateDelayedSamples` routine as the daily scheduled job: for each grower/partner user with samples collected 7+ days ago that have not arrived, it enqueues `processDelayedSampleEmailTask`, which emails via `UserQueueMessage`, inserts a per-sample notification, and flips `delayedEmailSent`. Produced on demand (manually or by tooling), not by another handler. If `data.scheduleTime` is set it drives the window; otherwise the document create time is used. See the [Delayed sample processing](/docs/flows/DelayedSampleProcessing) flow. --- id: SamplerAdded name: Sampler Added version: 0.0.1 summary: | A sampler was added. Written to events/sampler/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a sampler is added; consumed by the domain record notifier (`samplerAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SequenceAdded name: Sequence Added version: 0.0.1 summary: | A sequence was added. Written to events/sequence/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a sequence is added; consumed by the domain record notifier (`sequenceAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SequenceDropped name: Sequence Dropped version: 0.0.1 summary: | A sequence on the plate was dropped (failed QC). Written to events/sequence/dropped/{eventId} by Plate Processor. owners: - platform-team badges: - content: Feature-flagged backgroundColor: orange textColor: orange --- ## Overview Produced by **Plate Processor**; consumed by **Sequence Processor** (`sequenceDroppedEvent`). Returns immediately unless `SEND_DROPPED_SEQUENCE_NOTIFICATION` is `true` (currently `false`). When enabled, inserts a "could not be processed" notification per grower user. --- id: SequenceEdited name: Sequence Edited version: 0.0.1 summary: | A sequence was edited. Written to events/sequence/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a sequence is edited; consumed by the domain record notifier (`sequenceEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SequenceFailed name: Sequence Failed version: 0.0.1 summary: | A sequence on the plate failed processing. Written to events/sequence/failed/{eventId} by Plate Processor. owners: - platform-team badges: - content: No consumer backgroundColor: red textColor: red --- ## Overview Produced by **Plate Processor** (`queueSequencePredictionEvents`) for any sequence whose `failed` flag is set — a sibling of the `dropped` branch. Written with `skipSlackNotification: true`. ## No consumer — dead write Unlike `SequenceDropped`, **nothing subscribes to `events/sequence/failed`.** There is no `sequenceFailedEvent` handler in `functions-delphi`, so these documents are written and never read. Documented here so the gap is visible: either a handler is missing, or the write should be removed. Surfaced by `npm run audit:events`, which lists it as produced-with-no-consumer. --- id: SequencePredictionCompleted name: Sequence Prediction Completed version: 0.0.1 summary: | Yield prediction is ready for one sequence. Written to events/sequence/predictioncomplete/{eventId} by Plate Processor. owners: - platform-team --- ## Overview Produced by **Plate Processor** (`queueSequencePredictionEvents`); consumed by **Sequence Processor** (`sequencePredictionCompleteEvent`). Reads the yield prediction, computes a Yield Realisation Index (`YRI = 100 * prediction / yieldGoal`), and inserts a notification per grower user — unless the sample is older than `DAYS_BACK_THRESHOLD` or the sample has more than one active sequence. When `data.generateReport` is set (a test-only path) it emits `ZoneGenerateReport` instead of notifying directly. --- id: SequenceRepeatScanned name: Sequence Repeat Scanned version: 0.0.1 summary: | A sample has been scanned again (more than one sequence exists). Written to events/sequence/repeatscan/{eventId} by Plate Processor. owners: - platform-team badges: - content: Feature-flagged backgroundColor: orange textColor: orange --- ## Overview Produced by **Plate Processor**; consumed by **Sequence Processor** (`sequenceRepeatScanEvent`). The handler returns immediately unless `SEND_REPEAT_SCAN_NOTIFICATION` is `true` (currently `false` in `config.ts`), so no notification is sent today. Documented here so the wiring is not lost. --- id: SequenceScanned name: Sequence Scanned version: 0.0.1 summary: | First sequence seen for a sample. Written to events/sequence/scan/{eventId} by Plate Processor. owners: - platform-team --- ## Overview Produced by **Plate Processor** (`queueSequenceScanEvents`); consumed by **Sequence Processor** (`sequenceScanEvent`). Inserts a "Sample received" notification for each user on the grower, unless the sample is older than `DAYS_BACK_THRESHOLD`. Written with `skipSlackNotification: true`. --- id: SpectroscopyAdded name: Spectroscopy Added version: 0.0.1 summary: | A spectroscopy reading was added. Written to events/spectroscopy/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a spectroscopy reading is added; consumed by the domain record notifier (`spectroscopyAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: SpectroscopyEdited name: Spectroscopy Edited version: 0.0.1 summary: | A spectroscopy reading was edited. Written to events/spectroscopy/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a spectroscopy reading is edited; consumed by the domain record notifier (`spectroscopyEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: UserAdded name: User Added version: 0.0.1 summary: | A user was added. Written to events/user/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a user is added; consumed by the domain record notifier (`userAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: UserEdited name: User Edited version: 0.0.1 summary: | A user was edited. Written to events/user/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a user is edited; consumed by the domain record notifier (`userEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: UserQueueMessage name: User Queue Message version: 0.0.1 summary: | Request to notify a user across all of their channels. Written to events/user/queuemessage/{eventId}. The single fan-in point for user-facing notifications. owners: - platform-team --- ## Overview The shared entry point for "tell this user something." Produced by any handler via `queueMessagesForUsers(...)`; consumed by **Messaging Service** (`userQueueMessageEvent`), which expands it into `UserSendMessage` (one per FCM token) and `UserSendEmail`. ## Payload (observed) `userId`, `title`, `body`, optional `attachmentUrl`, optional `growerId`, and `skipSlackNotification`. --- id: UserSendEmail name: User Send Email version: 0.0.1 summary: | Deliver one email to a user. Written to events/user/sendemail/{eventId} by Messaging Service. owners: - platform-team --- ## Overview Produced by **Messaging Service** (`queueSendEmailEvent`); consumed by `userSendEmailEvent`, which enqueues the rate-limited `processSendEmailTask` command. That command sends via Mailgun and fetches/attaches the report PDF when `attachmentUrl` is present. --- id: UserSendMessage name: User Send Message version: 0.0.1 summary: | Deliver one push notification to a specific FCM token. Written to events/user/sendmessage/{eventId} by Messaging Service. owners: - platform-team --- ## Overview Produced by **Messaging Service** (`queueSendMessageEvents`, one per token); consumed by `userSendMessageEvent`, which sends an FCM push. If the token is unregistered/invalid it calls `DeleteFCMToken` in Data Connect to prune it. --- id: UserSendSMS name: User Send SMS version: 0.0.1 summary: | Request to deliver one SMS to a user. Written to events/user/sendsms/{eventId}. owners: - platform-team badges: - content: No producer backgroundColor: orange textColor: orange - content: Test recipient backgroundColor: orange textColor: orange --- ## Overview Consumed by **Messaging Service** (`userSendSMSEvent`), which sends the SMS via Vonage. **No handler currently produces this event** — the queuemessage fan-out emits `UserSendMessage` and `UserSendEmail` but not `UserSendSMS`. ## Caveats SMS only runs in the `delphi-1f2b6` project and is **hard-coded to a single test recipient** (`447866845818`), ignoring `data.phone`. Verify before relying on it. See the [SMS delivery](/docs/flows/SmsDelivery) flow. --- id: ZoneAdded name: Zone Added version: 0.0.1 summary: | A zone was added. Written to events/zone/add/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a zone is added; consumed by the domain record notifier (`zoneAddEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: ZoneEdited name: Zone Edited version: 0.0.1 summary: | A zone was edited. Written to events/zone/edit/{eventId}. owners: - platform-team --- ## Overview Produced by the **CropDx Backend** when a zone is edited; consumed by the domain record notifier (`zoneEditEvent`, built on the `createEventHandler` factory in `simpleEvents.ts`). The handler only posts a Slack summary — no downstream fan-out. See the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: ZoneGenerateReport name: Zone Generate Report version: 0.0.1 summary: | Request to render a PDF report for one zone. Written to events/zone/generatereport/{eventId}. owners: - platform-team --- ## Overview Produced by **Plate Processor** (`queueZoneReportEvents`, one per non-dropped zone whose grower is in the hard-coded `subscriptions` array) and, on a test-only path, by **Sequence Processor**. Consumed by **Field & Zone Service** (`zoneGenerateReportEvent`), which enqueues the rate-limited `processGenerateZoneReportTask`. See the [Zone report generation](/docs/flows/ZoneReportGeneration) flow for the full request-to-delivery chain. ## Payload (observed) `fieldId`, `fieldName`, `zoneId`, `zoneName`, `sampleId`, `growerId`, `allocationId`, `subscribers`, `messageGrower`, `messagePartner`, `skipSlackNotification`. --- id: ZoneReportGenerated name: Zone Report Generated version: 0.0.1 summary: | A zone report PDF has been rendered and stored. Written to events/zone/reportgenerated/{eventId}. owners: - platform-team --- ## Overview Both produced and consumed by **Field & Zone Service**: the `processGenerateZoneReportTask` command calls the `download-pdf-report` Cloud Run service and, on success, writes this event; `zoneReportGeneratedEvent` then consumes it and notifies subscribers, grower and partner via `UserQueueMessage`. This self-loop (one service produces and consumes the same event) is why it is easy to miss in the producer/consumer NodeGraph — see the [Zone report generation](/docs/flows/ZoneReportGeneration) flow for the sequence. ## Payload (observed) `allocationId`, `growerId`, `fieldId`, `fieldName`, `zoneId`, `zoneName`, `sampleId`, `pdfUrl`, `messageGrower`, `messagePartner`, `subscribers`. --- id: AllocationService name: Allocation Service version: 0.0.1 summary: | Handlers in src/handlers/allocation.ts: notify growers when a partner allocates zones, plus a partner-allocation Slack notifier. owners: - platform-team receives: - id: AllocationAdded version: 0.0.1 from: - id: firestore-events parameters: domain: allocation action: add - id: PartnerAllocated version: 0.0.1 from: - id: firestore-events parameters: domain: partner action: allocation sends: - id: UserQueueMessage version: 0.0.1 to: - id: firestore-events parameters: domain: user action: queuemessage repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/allocation.ts --- ## Overview Maps to `src/handlers/allocation.ts`. | Export | Listens on | Behaviour | |---|---|---| | `allocationAddEvent` | `events/allocation/add` | Slack + `notifyGrowersOfAllocation`: in-app notification and `UserQueueMessage` per grower user | | `partnerAllocationEvent` | `events/partner/allocation` | Slack notifier only (`createEventHandler`) | --- id: CropDxBackend name: CropDx Backend (external) version: 0.0.1 summary: | External producer. The application/backend services (referred to as "Delphi" and "Hermes") that write the top-level business events into Firestore which functions-delphi reacts to. owners: - platform-team sends: - id: PlateScanned version: 0.0.1 to: - id: firestore-events parameters: domain: plate action: scan - id: PlatePredictionCompleted version: 0.0.1 to: - id: firestore-events parameters: domain: plate action: predictioncomplete - id: PlateSequencingCompleted version: 0.0.1 to: - id: firestore-events parameters: domain: plate action: sequencingcomplete - id: FieldAdded version: 0.0.1 - id: DelphiError version: 0.0.1 - id: DelphiSuccess version: 0.0.1 - id: HermesError version: 0.0.1 badges: - content: External system backgroundColor: gray textColor: gray --- ## Overview Modelled as an **external system** because the code that writes these top-level events lives outside `functions-delphi`. It is the producer of most `add`, `scan`, `predictioncomplete` and `sequencingcomplete` events, plus the `error`/`success` telemetry. Confirm the exact list against the backend repo and split into separate "Delphi" / "Hermes" services if that distinction matters. --- id: DelayedSampleService name: Delayed Sample Service version: 0.0.1 summary: | Handlers in src/handlers/delayedSamples.ts: chase samples collected 7+ days ago that have not yet reached the lab, via a daily job or on demand. owners: - platform-team receives: - id: SampleProcessDelayed version: 0.0.1 from: - id: firestore-events parameters: domain: sample action: processdelayed sends: - id: UserQueueMessage version: 0.0.1 to: - id: firestore-events parameters: domain: user action: queuemessage repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/delayedSamples.ts --- ## Overview Maps to `src/handlers/delayedSamples.ts`. A daily scheduled job and the on-demand `events/sample/processdelayed` trigger both run `orchestrateDelayedSamples`, which enqueues the rate-limited `processDelayedSampleEmailTask` per affected user. That task emails via `UserQueueMessage`, inserts a per-sample in-app notification, and flips `delayedEmailSent` so the sample drops out of future runs. --- id: DiagnosticsRecordNotifier name: Diagnostics Record Notifier version: 0.0.1 summary: | Slack notifiers for lab-record record add/edit events, built on the createEventHandler factory in src/handlers/simpleEvents.ts. owners: - platform-team receives: - id: PlateAdded version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: add - id: PlateEdited version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: edit - id: SequenceAdded version: 0.0.1 from: - id: firestore-events parameters: domain: sequence action: add - id: SequenceEdited version: 0.0.1 from: - id: firestore-events parameters: domain: sequence action: edit - id: SampleAdded version: 0.0.1 from: - id: firestore-events parameters: domain: sample action: add - id: SampleEdited version: 0.0.1 from: - id: firestore-events parameters: domain: sample action: edit - id: SpectroscopyAdded version: 0.0.1 from: - id: firestore-events parameters: domain: spectroscopy action: add - id: SpectroscopyEdited version: 0.0.1 from: - id: firestore-events parameters: domain: spectroscopy action: edit - id: HarvestAdded version: 0.0.1 from: - id: firestore-events parameters: domain: harvest action: add - id: HarvestEdited version: 0.0.1 from: - id: firestore-events parameters: domain: harvest action: edit repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/simpleEvents.ts --- ## Overview Thin Slack-notifier handlers from `simpleEvents.ts`: each consumes a `plate/sequence/sample/...` add or edit event produced by the **CropDx Backend** and posts a summary to Slack. No downstream fan-out — see the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: FieldZoneService name: Field & Zone Service version: 0.0.1 summary: | Handlers in src/handlers/fieldAndZone.ts: notify partners of new fields, and drive zone PDF report generation via a rate-limited Cloud Run task. owners: - platform-team receives: - id: FieldAdded version: 0.0.1 from: - id: firestore-events parameters: domain: field action: add - id: ZoneGenerateReport version: 0.0.1 from: - id: firestore-events parameters: domain: zone action: generatereport - id: ZoneReportGenerated version: 0.0.1 from: - id: firestore-events parameters: domain: zone action: reportgenerated sends: - id: ZoneReportGenerated version: 0.0.1 to: - id: firestore-events parameters: domain: zone action: reportgenerated - id: UserQueueMessage version: 0.0.1 to: - id: firestore-events parameters: domain: user action: queuemessage repository: language: TypeScript url: >- https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/fieldAndZone.ts --- ## Overview | Export | Listens on | Behaviour / fans out to | |---|---|---| | `fieldAddEvent` | `events/field/add` | Notifies partner users → `UserQueueMessage` | | `zoneGenerateReportEvent` | `events/zone/generatereport` | Enqueues `processGenerateZoneReportTask` | | `zoneReportGeneratedEvent` | `events/zone/reportgenerated` | Notifies subscribers/grower/partner → `UserQueueMessage` | The `processGenerateZoneReportTask` command calls the `download-pdf-report` Cloud Run service and, on success, writes `ZoneReportGenerated` back to Firestore — so this service both **produces and consumes** that event. `ZoneGenerateReport` is produced upstream by **Plate Processor** and (on the test path) **Sequence Processor**. --- id: GrowerRecordNotifier name: Grower Record Notifier version: 0.0.1 summary: | Slack notifiers for commercial-record record add/edit events, built on the createEventHandler factory in src/handlers/simpleEvents.ts. owners: - platform-team receives: - id: PartnerAdded version: 0.0.1 from: - id: firestore-events parameters: domain: partner action: add - id: GrowerAdded version: 0.0.1 from: - id: firestore-events parameters: domain: grower action: add - id: PurchaseAdded version: 0.0.1 from: - id: firestore-events parameters: domain: purchase action: add - id: SamplerAdded version: 0.0.1 from: - id: firestore-events parameters: domain: sampler action: add - id: FieldEdited version: 0.0.1 from: - id: firestore-events parameters: domain: field action: edit - id: ZoneAdded version: 0.0.1 from: - id: firestore-events parameters: domain: zone action: add - id: ZoneEdited version: 0.0.1 from: - id: firestore-events parameters: domain: zone action: edit repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/simpleEvents.ts --- ## Overview Thin Slack-notifier handlers from `simpleEvents.ts`: each consumes a `partner/grower/zone/...` add or edit event produced by the **CropDx Backend** and posts a summary to Slack. No downstream fan-out — see the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: LeafWebhookService name: Leaf Webhook Service version: 0.0.1 summary: | HTTP endpoints and handlers in src/handlers/leaf.ts for Leaf (withleaf.io) operation and irrigation alerts. owners: - platform-team sends: - id: LeafOperationReceived version: 0.0.1 to: - id: firestore-events parameters: domain: webhook action: leafoperation - id: LeafIrrigationReceived version: 0.0.1 to: - id: firestore-events parameters: domain: webhook action: leafirrigation receives: - id: LeafOperationReceived version: 0.0.1 from: - id: firestore-events parameters: domain: webhook action: leafoperation - id: LeafIrrigationReceived version: 0.0.1 from: - id: firestore-events parameters: domain: webhook action: leafirrigation repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/leaf.ts --- ## Overview | Export | Type | Behaviour | |---|---|---| | `leafWebhook` | HTTPS `onRequest` | Verifies `X-Leaf-Signature`, writes `events/webhook/leafoperation` | | `leafIrrigationWebhook` | HTTPS `onRequest` | Verifies signature, writes `events/webhook/leafirrigation` | | `leafWebhookEvent` | Firestore trigger | Consumes `leafoperation`; authenticates to Leaf, fetches operation + summary, matches the field in Data Connect | | `leafIrrigationWebhookEvent` | Firestore trigger | Consumes `leafirrigation`; fetches irrigation summary from Leaf | This service both writes (`sends`) and consumes (`receives`) its events — the HTTP endpoint is the producer and the Firestore trigger is the consumer. --- id: MailgunWebhookService name: Mailgun Webhook Service version: 0.0.1 summary: | HTTP endpoint and handler in src/handlers/mailgun.ts for Mailgun delivery events (opens, bounces, etc.). owners: - platform-team sends: - id: MailgunWebhookReceived version: 0.0.1 to: - id: firestore-events parameters: domain: webhook action: mailgun receives: - id: MailgunWebhookReceived version: 0.0.1 from: - id: firestore-events parameters: domain: webhook action: mailgun repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/mailgun.ts --- ## Overview `mailgunWebhook` (HTTPS) verifies the Mailgun HMAC signature and writes the full payload to `events/webhook/mailgun`. `mailgunWebhookEvent` consumes it and posts a Slack summary. Currently a pass-through for observability. --- id: MessagingService name: Messaging Service version: 0.0.1 summary: | Handlers in src/handlers/userMessaging.ts. Fans a queuemessage request out to push, email and SMS, and delivers each channel. owners: - platform-team receives: - id: UserQueueMessage version: 0.0.1 from: - id: firestore-events parameters: domain: user action: queuemessage - id: UserSendMessage version: 0.0.1 from: - &ref_0 id: firestore-events parameters: domain: user action: sendmessage - id: UserSendEmail version: 0.0.1 from: - &ref_1 id: firestore-events parameters: domain: user action: sendemail - id: UserSendSMS version: 0.0.1 from: - id: firestore-events parameters: domain: user action: sendsms sends: - id: UserSendMessage version: 0.0.1 to: - *ref_0 - id: UserSendEmail version: 0.0.1 to: - *ref_1 repository: language: TypeScript url: >- https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/userMessaging.ts --- ## Overview | Export | Listens on | Behaviour / fans out to | |---|---|---| | `userQueueMessageEvent` | `events/user/queuemessage` | Loads user; emits `UserSendMessage` per FCM token and `UserSendEmail` | | `userSendMessageEvent` | `events/user/sendmessage` | Sends FCM push; deletes dead tokens from Data Connect | | `userSendEmailEvent` | `events/user/sendemail` | Enqueues `processSendEmailTask` (Mailgun) | | `userSendSMSEvent` | `events/user/sendsms` | Sends SMS via Vonage | `UserQueueMessage` is produced by **Plate Processor**, **Field & Zone Service**, and indirectly by anything calling `queueMessagesForUsers`. ## Notes for maintainers The `processSendEmailTask` command is rate-limited (1/sec) to avoid Mailgun 420 errors and retries on 420/5xx. SMS is hard-coded to a test recipient and only runs in the local env today — verify before relying on it. --- id: PlateProcessor name: Plate Processor version: 0.0.1 summary: | Firestore-triggered handlers in src/handlers/plate.ts. Consumes plate-level events and fans them out into sequence-level events, user notifications, and rate-limited sequencing-results tasks. owners: - platform-team receives: - id: PlateScanned version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: scan - id: PlatePredictionCompleted version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: predictioncomplete - id: PlateSequencingCompleted version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: sequencingcomplete - id: PlateSequencingResultsPulled version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: sequencingresults sends: - id: SequenceScanned version: 0.0.1 to: - id: firestore-events parameters: domain: sequence action: scan - id: SequenceRepeatScanned version: 0.0.1 to: - id: firestore-events parameters: domain: sequence action: repeatscan - id: SequencePredictionCompleted version: 0.0.1 to: - id: firestore-events parameters: domain: sequence action: predictioncomplete - id: SequenceDropped version: 0.0.1 to: - id: firestore-events parameters: domain: sequence action: dropped - id: ZoneGenerateReport version: 0.0.1 to: - id: firestore-events parameters: domain: zone action: generatereport - id: SequenceFailed version: 0.0.1 to: - id: firestore-events parameters: domain: sequence action: failed - id: PlateSequencingResultsPulled version: 0.0.1 to: - id: firestore-events parameters: domain: plate action: sequencingresults - id: UserQueueMessage version: 0.0.1 to: - id: firestore-events parameters: domain: user action: queuemessage repository: language: TypeScript url: >- https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/plate.ts --- ## Overview Maps to `src/handlers/plate.ts`. Each exported function is an `onDocumentCreated` trigger on a Firestore path. | Export | Listens on (`receives`) | Fans out to (`sends`) | |---|---|---| | `plateScanEvent` | `events/plate/scan` | `events/sequence/scan`, `events/sequence/repeatscan`, `events/user/queuemessage` | | `platePredictionCompleteEvent` | `events/plate/predictioncomplete` | `events/zone/generatereport`, `events/sequence/predictioncomplete`, `events/sequence/dropped`, `events/user/queuemessage` | | `plateSequencingCompleteEvent` | `events/plate/sequencingcomplete` | enqueues `processPullSequencingResultsTask` | | `plateSequencingResultsPulledEvent` | `events/plate/sequencingresults` | writes sequencing stats to Data Connect | > The `*` fan-out events are written with `skipSlackNotification: true` so the > downstream handler does not double-post to Slack. The intermediate > `processPullSequencingResultsTask` command pulls run stats from Illumina > BaseSpace and then writes `events/plate/sequencingresults`. --- id: SequenceProcessor name: Sequence Processor version: 0.0.1 summary: | Firestore-triggered handlers in src/handlers/sequence.ts. Consumes the per-sequence events fanned out by Plate Processor and writes grower notifications into Data Connect. owners: - platform-team receives: - id: SequenceScanned version: 0.0.1 from: - id: firestore-events parameters: domain: sequence action: scan - id: SequenceRepeatScanned version: 0.0.1 from: - id: firestore-events parameters: domain: sequence action: repeatscan - id: SequencePredictionCompleted version: 0.0.1 from: - id: firestore-events parameters: domain: sequence action: predictioncomplete - id: SequenceDropped version: 0.0.1 from: - id: firestore-events parameters: domain: sequence action: dropped sends: - id: ZoneGenerateReport version: 0.0.1 to: - id: firestore-events parameters: domain: zone action: generatereport repository: language: TypeScript url: >- https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/sequence.ts --- ## Overview Maps to `src/handlers/sequence.ts`. These handlers are mostly terminal: they load the sequence from Data Connect and call `insertNotification` for each user on the grower, unless the sample is older than `DAYS_BACK_THRESHOLD`. | Export | Listens on | Behaviour | |---|---|---| | `sequenceScanEvent` | `events/sequence/scan` | "Sample received" notification | | `sequenceRepeatScanEvent` | `events/sequence/repeatscan` | "Being re-processed" — gated by `SEND_REPEAT_SCAN_NOTIFICATION` | | `sequencePredictionCompleteEvent` | `events/sequence/predictioncomplete` | Computes YRI and notifies; can emit `ZoneGenerateReport` on the test path | | `sequenceDroppedEvent` | `events/sequence/dropped` | "Could not be processed" — gated by `SEND_DROPPED_SEQUENCE_NOTIFICATION` | ## Notes for maintainers `sequenceAddEvent` / `sequenceEditEvent` (`events/sequence/add`, `.../edit`) are simple Slack notifiers and are exported from `simpleEvents.ts`, not here. --- id: Slack name: Slack (external) version: 0.0.1 summary: | External sink. Nearly every handler posts a notification to the SLACK_PLATES_WEBHOOK_URL Slack channel via src/slack.ts. owners: - platform-team receives: - id: PlateScanned version: 0.0.1 from: - id: firestore-events parameters: domain: plate action: scan - id: FieldAdded version: 0.0.1 - id: DelphiError version: 0.0.1 badges: - content: External system backgroundColor: gray textColor: gray --- ## Overview A cross-cutting consumer. `sendSlackNotification` is called from almost every handler, so in practice Slack consumes the majority of events. Rather than list every event here, treat Slack as a known global sink and only model the links that matter for a given diagram. The `skipSlackNotification` flag on a Firestore document suppresses this post for internally-generated fan-out events. --- id: TelemetryNotifier name: Telemetry Notifier version: 0.0.1 summary: | Simple Slack notifiers for backend success/error signals, built on the createEventHandler factory in src/handlers/simpleEvents.ts. owners: - platform-team receives: - id: DelphiError version: 0.0.1 from: - id: firestore-events parameters: domain: error action: delphi - id: DelphiSuccess version: 0.0.1 from: - id: firestore-events parameters: domain: success action: delphi - id: HermesError version: 0.0.1 from: - id: firestore-events parameters: domain: error action: hermes repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/simpleEvents.ts --- ## Overview `delphiErrorEvent`, `delphiSuccessEvent` and `hermesErrorEvent` are produced by the **CropDx Backend** and consumed here. Each one only posts to Slack — the red/green colour is chosen by whether the event title contains "error". No downstream fan-out. --- id: UserRecordNotifier name: User Record Notifier version: 0.0.1 summary: | Slack notifiers for user-record record add/edit events, built on the createEventHandler factory in src/handlers/simpleEvents.ts. owners: - platform-team receives: - id: UserAdded version: 0.0.1 from: - id: firestore-events parameters: domain: user action: add - id: UserEdited version: 0.0.1 from: - id: firestore-events parameters: domain: user action: edit repository: language: TypeScript url: https://github.com/cropdx/dataconnect/tree/main/functions-delphi/src/handlers/simpleEvents.ts --- ## Overview Thin Slack-notifier handlers from `simpleEvents.ts`: each consumes a `user` add or edit event produced by the **CropDx Backend** and posts a summary to Slack. No downstream fan-out — see the [Record notifier pattern](/docs/flows/RecordNotifierPattern) flow. --- id: Diagnostics name: Diagnostics version: 0.0.1 summary: | The laboratory pipeline: plates and sequences move from scan, through sequencing, to RNA yield predictions. This is where the choreography lives — one plate event fans out into many sequence and zone events. owners: - platform-team services: - id: PlateProcessor version: 0.0.1 - id: SequenceProcessor version: 0.0.1 - id: DelayedSampleService version: 0.0.1 - id: DiagnosticsRecordNotifier version: 0.0.1 flows: - id: DelayedSampleProcessing version: 0.0.1 - id: PlateScanFanout version: 0.0.1 - id: PlateSequencingCompleted version: 0.0.1 - id: PlatePredictionFanout version: 0.0.1 badges: - content: Core pipeline backgroundColor: green textColor: green --- ## Overview The **Diagnostics** domain owns every event whose Firestore path begins with `events/plate/*`, `events/sequence/*`, `events/sample/*`, `events/spectroscopy/*` and `events/harvest/*`. A plate is a physical tray of samples. As it moves through the lab, the CropDx backend writes `plate.*` events. `functions-delphi` consumes those and fans them out into per-sequence and per-zone events, which in turn trigger grower notifications and report generation. This domain is the heart of the choreography, so keep its `` accurate. ## Boundary rationale These message types share one bounded context: the lab/sequencing workflow and its data (plates, sequences, samples, spectroscopy, harvests). They are produced and consumed together, and they reference the same identifiers (`plateId`, `sequenceId`, `sampleId`). Grouping them keeps the fan-out visible in one place. --- id: GrowerManagement name: Grower Management version: 0.0.1 summary: | The commercial and agronomy side: partners, purchases, allocations, samplers, growers, fields and zones — plus zone PDF report generation. owners: - platform-team services: - id: FieldZoneService version: 0.0.1 - id: AllocationService version: 0.0.1 - id: GrowerRecordNotifier version: 0.0.1 flows: - id: ZoneReportGeneration version: 0.0.1 - id: AllocationAdded version: 0.0.1 - id: FieldAdded version: 0.0.1 --- ## Overview Owns events under `events/partner/*`, `events/purchase/*`, `events/allocation/*`, `events/sampler/*`, `events/grower/*`, `events/field/*` and `events/zone/*`. Most of these (`partner.add`, `grower.add`, `purchase.add`, `allocation.add`, `sampler.add`, `zone.add`, `partner.allocation`) are simple Slack notifiers exported from `src/handlers/simpleEvents.ts`. The richer flows — `field.add`, `zone.generatereport`, `zone.reportgenerated` — live in `src/handlers/fieldAndZone.ts` and are modelled under **Field & Zone Service**. ## Boundary rationale These entities form one ownership chain (partner → purchase → allocation → grower → field → zone) and are administered by the same teams. An alternative is to split **Commercial** (partner/purchase/allocation/sampler) from **Agronomy** (grower/field/zone) if those become separate ownership areas; that is recorded as an option in the design doc, not adopted here. > **Not deployed:** `field.edit`, `zone.edit` and `sample.add` handlers are > defined but commented out of `src/index.ts`, so they are not currently > deployed. Mark them `draft` in EventCatalog rather than deleting them. --- id: Integrations name: Integrations version: 0.0.1 summary: | Inbound webhooks from third parties (Leaf, Mailgun) and the events they raise. owners: - platform-team services: - id: LeafWebhookService version: 0.0.1 - id: MailgunWebhookService version: 0.0.1 flows: - id: LeafWebhookIngress version: 0.0.1 - id: MailgunWebhookIngress version: 0.0.1 --- ## Overview Owns `events/webhook/*`. Two HTTP endpoints (`leafWebhook`, `leafIrrigationWebhook`) and `mailgunWebhook` verify a signature, then translate the inbound request into a Firestore event (`webhook.leafoperation`, `webhook.leafirrigation`, `webhook.mailgun`). Separate handlers consume those events and call back out to the Leaf API. ## Boundary rationale Anti-corruption layer: external payloads are normalised into our own event shapes at the boundary, so the rest of the system never depends on Leaf's or Mailgun's wire format directly. --- id: Notifications name: Notifications version: 0.0.1 summary: | Per-user message delivery: the queuemessage fan-out plus push (FCM), email (Mailgun) and SMS (Vonage) channels. owners: - platform-team services: - id: MessagingService version: 0.0.1 - id: UserRecordNotifier version: 0.0.1 flows: - id: SmsDelivery version: 0.0.1 --- ## Overview Owns `events/user/*`. This is the convergence point of the whole catalog: every domain that wants to reach a user does so by writing `events/user/queuemessage` (via the shared `queueMessagesForUsers` helper). `MessagingService` then expands that into per-channel delivery events. ``` UserQueueMessage ├── UserSendMessage (one per FCM token) → FCM push └── UserSendEmail → processSendEmailTask → Mailgun ``` `UserSendSMS` (`events/user/sendsms`) is wired to Vonage but is not produced by any current handler. --- id: Platform name: Platform version: 0.0.1 summary: | System telemetry events: success and error signals from the Delphi and Hermes backends, surfaced to Slack. owners: - platform-team services: - id: TelemetryNotifier version: 0.0.1 flows: - id: RecordNotifierPattern version: 0.0.1 --- ## Overview Owns `events/error/delphi`, `events/success/delphi` and `events/error/hermes`. These are cross-cutting operational signals rather than business events; keeping them in their own domain stops them from cluttering the business ``s. --- id: firestore-events name: Firestore events collection version: 0.0.1 summary: | The transport for all internal events: Firestore documents created under events/{domain}/{action}/{eventId}. A document create is the "publish". owners: - platform-team address: events/{domain}/{action}/{eventId} protocols: - firestore parameters: domain: description: First path segment, e.g. plate, sequence, user, zone, webhook. action: description: Second path segment, e.g. scan, predictioncomplete, queuemessage. --- ## Overview Every internal event in `functions-delphi` is transported the same way: a producer writes a document to `events/{domain}/{action}/{eventId}`, and an `onDocumentCreated` Cloud Function on that path is the subscriber. There is no broker — Firestore is the bus. Modelling this as a single parameterised channel lets each event declare which `{domain}`/`{action}` slot it uses, so the catalog mirrors the real Firestore layout. Webhook ingress (`leafWebhook`, `mailgunWebhook`) and the `onTaskDispatched` task queues are separate transports — document them as their own channels if you want them on the diagrams. --- id: AllocationAdded name: Allocation added version: 0.0.1 summary: | What happens when a partner allocates zones to a grower: a Slack post plus an in-app notification and a queued message for every grower user. owners: - platform-team badges: - content: Notifications backgroundColor: green textColor: green steps: - id: 1 title: Allocation created summary: | The CropDx backend writes events/allocation/add/{eventId} with `payload.allocationId`. message: id: AllocationAdded version: 0.0.1 next_step: id: 2 label: triggers allocationAddEvent - id: 2 title: Allocation Service fans out summary: | Posts to Slack and calls `notifyGrowersOfAllocation`, which loads the allocation (crop, zone count, partner) and the grower's users. service: id: AllocationService version: 0.0.1 next_steps: - id: 3 label: in-app, per grower user - id: 4 label: queued message, per grower user - id: 5 label: Slack summary - id: 3 title: In-app notification per grower user summary: | `insertNotification` writes "{partner} has allocated {n} {crop} zones to you" straight into Data Connect for each unique grower user. custom: title: insertNotification (Data Connect) type: Data Connect write icon: BoltIcon color: green summary: Per-user in-app notification, written directly (no event). - id: 4 title: Queue a message per grower user summary: | `queueMessagesForUsers` writes one events/user/queuemessage per grower user — "New allocation. Please add fields via the CropDx Insights app." message: id: UserQueueMessage version: 0.0.1 next_step: id: 6 label: triggers userQueueMessageEvent - id: 5 title: Slack (#plates) externalSystem: name: Slack (#plates) summary: Operational visibility channel. - id: 6 title: Messaging Service delivers summary: | Expands each queued message into UserSendMessage (per FCM token) and UserSendEmail. service: id: MessagingService version: 0.0.1 --- ## Overview `allocationAddEvent` is the substantive member of `allocation.ts` (its sibling `partnerAllocationEvent` is just a Slack notifier — see the [Record notifier pattern](/docs/flows/RecordNotifierPattern)). Each grower user is reached two ways from one allocation: a direct Data Connect notification (step 3) and a `UserQueueMessage` (step 4) that fans out again into push and email. --- id: DelayedSampleProcessing name: Delayed sample processing version: 0.0.1 summary: | Chasing samples that were collected 7+ days ago but haven't reached the lab: a daily job (or on-demand event) fans out a rate-limited email task per user that notifies and then flips a flag so each sample is chased only once. owners: - platform-team badges: - content: Scheduled + on-demand backgroundColor: blue textColor: blue steps: - id: 1 title: Trigger — daily schedule or on demand summary: | A daily Cloud Scheduler job (00:00 America/Los_Angeles) runs the orchestration. The same run can be triggered on demand by creating events/sample/processdelayed/{docId} — optionally with `scheduleTime` to set the window. message: id: SampleProcessDelayed version: 0.0.1 next_step: id: 2 label: triggers processDelayedSamplesOnDemand (or the scheduled job) - id: 2 title: Orchestrate delayed samples summary: | `orchestrateDelayedSamples` finds samples collected 7+ days ago without `delayedEmailSent`, groups them by grower user and partner user, and enqueues one email task per user. Skipped entirely outside the `delphi-1f2b6` project and in the local emulator. service: id: DelayedSampleService version: 0.0.1 next_step: id: 3 label: enqueue processDelayedSampleEmailTask per user - id: 3 title: Delayed-sample email task (rate-limited) summary: | `processDelayedSampleEmailTask` runs at most 1/sec with retries and a per-user checkpoint marker, so a retry never re-emails or re-notifies a sample that already succeeded. custom: title: processDelayedSampleEmailTask type: Cloud Tasks queue icon: ClockIcon color: blue summary: Idempotent per-user worker with a resume marker. next_steps: - id: 4 label: email the user (once) - id: 6 label: per sample — notify, then flag - id: 4 title: Queue the delayed-sample email summary: | Emails the user once via the shared queuemessage pipeline — `queueMessagesForUsers` writes events/user/queuemessage ("Delayed samples"). message: id: UserQueueMessage version: 0.0.1 next_step: id: 5 label: triggers userQueueMessageEvent - id: 5 title: Messaging Service delivers service: id: MessagingService version: 0.0.1 - id: 6 title: Notify, then flip the flag (per sample) summary: | Inserts a per-sample in-app notification (partner users see the grower name), then last of all sets `delayedEmailSent = true` via `UpdateSample` so the sample drops out of future runs. Flag flips last so a failure retries rather than silently skipping. custom: title: insertNotification + UpdateSample (Data Connect) type: Data Connect writes icon: BoltIcon color: green summary: Per-sample notification and the checkpoint flag flip. --- ## Overview This flow has no upstream producer event on the bus — it starts from a schedule, with `events/sample/processdelayed` as an on-demand equivalent. The value it makes visible is the **idempotency structure**: a rate-limited task with a resume marker, emailing once and flipping `delayedEmailSent` last so retries are safe. None of that is visible in a producer/consumer view. --- id: FieldAdded name: Field added version: 0.0.1 summary: | When a grower adds a field, the partner's users are notified — both in-app and through the shared queuemessage pipeline. owners: - platform-team badges: - content: Notifications backgroundColor: green textColor: green steps: - id: 1 title: Field added summary: | The CropDx backend writes events/field/add/{eventId} with `growerId` and `allocationId`. Missing either, the handler logs and stops. message: id: FieldAdded version: 0.0.1 next_step: id: 2 label: triggers fieldAddEvent - id: 2 title: Field & Zone Service notifies the partner summary: | Posts to Slack, resolves the allocation's partner, and — for each partner user — inserts an in-app notification and queues a message. Grower name and field name are interpolated into the text. service: id: FieldZoneService version: 0.0.1 next_steps: - id: 3 label: per partner user - id: 4 label: per partner user - id: 3 title: In-app notification per partner user summary: | `insertNotification` writes "{grower} added a new field called '{field}'" into Data Connect. custom: title: insertNotification (Data Connect) type: Data Connect write icon: BoltIcon color: green summary: Per-user in-app notification, written directly (no event). - id: 4 title: Queue a message per partner user summary: | `queueMessagesForUsers` writes one events/user/queuemessage per partner user — "New field added." message: id: UserQueueMessage version: 0.0.1 next_step: id: 5 label: triggers userQueueMessageEvent - id: 5 title: Messaging Service delivers service: id: MessagingService version: 0.0.1 --- ## Overview `fieldAddEvent` (in `fieldAndZone.ts`) targets the **partner** side: when a grower adds a field, the partner's users are told, both directly in Data Connect and via `UserQueueMessage`. It only fires when both `growerId` and `allocationId` are present on the payload. --- id: LeafWebhookIngress name: Leaf webhook ingress version: 0.0.1 summary: | How a Leaf (withleaf.io) alert becomes an internal event and gets enriched: signature-verified HTTP ingress → Firestore event → a consumer that calls back to the Leaf API. Covers both operation and irrigation alerts. owners: - platform-team badges: - content: Webhook ingress backgroundColor: blue textColor: blue steps: - id: 1 title: Leaf sends an alert summary: | Leaf POSTs an operation or irrigation alert to our HTTPS endpoint, signed with X-Leaf-Signature. externalSystem: name: Leaf (withleaf.io) summary: Third-party field-operations platform. url: https://api.withleaf.io next_steps: - id: 2 label: operation alert - id: 5 label: irrigation alert - id: 2 title: leafWebhook endpoint summary: | Verifies the HMAC X-Leaf-Signature (constant-time), rejects bad method or signature, then writes the event. A body with `message` is acked and dropped. custom: title: leafWebhook type: HTTPS onRequest (us-east4) icon: BoltIcon color: blue summary: Signature-verified HTTP ingress endpoint. next_step: id: 3 label: writes events/webhook/leafoperation - id: 3 title: Leaf operation received message: id: LeafOperationReceived version: 0.0.1 next_step: id: 4 label: triggers leafWebhookEvent - id: 4 title: Leaf Webhook Service enriches the operation summary: | Posts to Slack (unless skipped). For `operationProcessingFinished`, authenticates to the Leaf API, fetches the operation and its summary, and matches the field via getLeafFieldByLeafFieldId in Data Connect. Terminal — logs the summary, emits no further event. service: id: LeafWebhookService version: 0.0.1 next_step: id: 8 label: authenticate + fetch operation/summary - id: 5 title: leafIrrigationWebhook endpoint custom: title: leafIrrigationWebhook type: HTTPS onRequest (us-east4) icon: BoltIcon color: blue summary: Signature-verified HTTP ingress endpoint. next_step: id: 6 label: writes events/webhook/leafirrigation - id: 6 title: Leaf irrigation received message: id: LeafIrrigationReceived version: 0.0.1 next_step: id: 7 label: triggers leafIrrigationWebhookEvent - id: 7 title: Leaf Webhook Service enriches the irrigation summary: | Posts to Slack (unless skipped). For `newFieldIrrigationActivity`, authenticates to the Leaf API and fetches the irrigation summary. Terminal. service: id: LeafWebhookService version: 0.0.1 next_step: id: 8 label: authenticate + fetch irrigation summary - id: 8 title: Leaf API summary: | The consumer calls back out to Leaf for the operation/irrigation detail. This outbound enrichment is invisible on the producer/consumer graph. externalSystem: name: Leaf API summary: Outbound calls for operation and irrigation detail. url: https://api.withleaf.io --- ## Overview Webhook ingress is a two-transport story the NodeGraph can't tell: an external HTTP request crosses into the system, becomes a Firestore event, and the consumer then calls *back out* to Leaf. The HTTP endpoint and the outbound Leaf API calls are both off the event bus, so only this flow shows the whole path. ## Anti-corruption boundary The endpoints normalise Leaf's wire format into our own `webhook/*` event shapes before anything downstream sees them — so the rest of the system never depends on Leaf's payload structure directly. --- id: MailgunWebhookIngress name: Mailgun webhook ingress version: 0.0.1 summary: | How a Mailgun delivery event (open, bounce, etc.) enters the system: signature-verified HTTP ingress → Firestore event → Slack summary. owners: - platform-team badges: - content: Webhook ingress backgroundColor: blue textColor: blue steps: - id: 1 title: Mailgun sends a delivery event summary: | Mailgun POSTs a signed delivery event (open, bounce, delivered, ...) to our endpoint. externalSystem: name: Mailgun summary: Transactional email provider (delivery webhooks). next_step: id: 2 label: HTTP POST (signed) - id: 2 title: mailgunWebhook endpoint summary: | Verifies the Mailgun HMAC signature (timestamp + token), then writes the full payload to the event, spread into the document. custom: title: mailgunWebhook type: HTTPS onRequest (us-east4) icon: BoltIcon color: blue summary: Signature-verified HTTP ingress endpoint. next_step: id: 3 label: writes events/webhook/mailgun - id: 3 title: Mailgun webhook received message: id: MailgunWebhookReceived version: 0.0.1 next_step: id: 4 label: triggers mailgunWebhookEvent - id: 4 title: Mailgun Webhook Service posts to Slack summary: | `mailgunWebhookEvent` posts a Slack summary. Currently a pass-through for observability — no downstream fan-out. service: id: MailgunWebhookService version: 0.0.1 next_step: id: 5 label: Slack summary - id: 5 title: Slack (#plates) externalSystem: name: Slack (#plates) summary: Operational visibility channel. --- ## Overview The same webhook-ingress shape as Leaf, but simpler: Mailgun's signed request is verified, normalised into a `webhook/mailgun` event, and the consumer just echoes it to Slack. No outbound enrichment today — the event exists so delivery signals are captured on the bus and can grow a real consumer later. --- id: PlatePredictionFanout name: Plate prediction fan-out version: 0.0.1 summary: | What actually happens when RNA yield predictions land for a plate: one plate-level event becomes N sequence events, M zone report requests, and a notification per affected user. owners: - platform-team badges: - content: Core pipeline backgroundColor: green textColor: green steps: - id: 1 title: Predictions land for a plate summary: | The CropDx backend writes events/plate/predictioncomplete/{eventId} once RNA yield predictions are ready for every sequence on the plate. message: id: PlatePredictionCompleted version: 0.0.1 next_step: id: 2 label: triggers platePredictionCompleteEvent - id: 2 title: Plate Processor loads the plate and fans out summary: | `platePredictionCompleteEvent` loads the plate with its users, then fans out three independent ways. All three writes carry `skipSlackNotification: true` so downstream handlers don't double-post. service: id: PlateProcessor version: 0.0.1 next_steps: - id: 3 label: one per non-dropped sequence - id: 4 label: one per dropped sequence - id: 5 label: per non-dropped zone, subscribed growers only - id: 6 label: per user with new RNA insights - id: 3 title: Sequence prediction ready summary: | Written to events/sequence/predictioncomplete/{eventId} by `queueSequencePredictionEvents` — one document per sequence that passed QC. message: id: SequencePredictionCompleted version: 0.0.1 next_step: id: 7 label: triggers sequencePredictionCompleteEvent - id: 4 title: Sequence dropped summary: | Written to events/sequence/dropped/{eventId} for sequences that failed QC. The consumer returns immediately today — SEND_DROPPED_SEQUENCE_NOTIFICATION is false. message: id: SequenceDropped version: 0.0.1 next_step: id: 7 label: triggers sequenceDroppedEvent (no-op while flagged off) - id: 5 title: Zone report requested summary: | `queueZoneReportEvents` writes events/zone/generatereport/{eventId}, but only for growers listed in the hard-coded `subscriptions` array. Growers missing from that array silently get no report. custom: title: ZoneGenerateReport type: Event (undocumented) icon: BoltIcon color: orange summary: | Not yet modelled as an event in this catalog — Plate Processor lists it under `sends` with no channel. Consumed by Field & Zone Service. properties: Firestore path: events/zone/generatereport/{eventId} Gate: hard-coded grower subscription list next_step: id: 8 label: triggers zoneGenerateReportEvent - id: 6 title: Notify users of new RNA insights summary: | The shared fan-in point for anything user-facing. Written via `queueMessagesForUsers` to events/user/queuemessage/{eventId}. message: id: UserQueueMessage version: 0.0.1 next_step: id: 9 label: triggers userQueueMessageEvent - id: 7 title: Sequence Processor notifies the grower summary: | Computes the Yield Realisation Index (YRI = 100 * prediction / yieldGoal) and calls `insertNotification` per grower user — writing straight to Data Connect rather than emitting another event. Skipped when the sample is older than DAYS_BACK_THRESHOLD or has more than one active sequence. service: id: SequenceProcessor version: 0.0.1 - id: 8 title: Field & Zone Service generates the PDF summary: | `zoneGenerateReportEvent` enqueues the rate-limited `processGenerateZoneReportTask`, which calls the `download-pdf-report` Cloud Run service and writes ZoneReportGenerated back to Firestore. This service consumes its own output, then notifies subscribers. service: id: FieldZoneService version: 0.0.1 next_step: id: 6 label: report ready — notifies subscribers, grower and partner - id: 9 title: Messaging Service delivers summary: | Loads the user and expands one queued message into UserSendMessage (one per FCM token) and UserSendEmail, each delivered by its own handler. service: id: MessagingService version: 0.0.1 --- ## Overview The `` on each event answers *who publishes this and who consumes it*. It cannot answer *what does this event cause*, because in `functions-delphi` causation happens inside the handler — `platePredictionCompleteEvent` reads one document and decides to write four others. Every one of those writes goes through the same [Firestore events collection](/docs/channels/firestore-events), so the transport view collapses them into a single hub. This flow is that missing view: what one plate event actually sets off, with the cardinality on each edge. ## Where the fan-out multiplies | Branch | Cardinality per plate event | Gate | |---|---|---| | `SequencePredictionCompleted` | one per non-dropped sequence | none | | `SequenceDropped` | one per dropped sequence | consumer no-ops while `SEND_DROPPED_SEQUENCE_NOTIFICATION` is `false` | | `ZoneGenerateReport` | one per non-dropped zone | hard-coded `subscriptions` array in `queueZoneReportEvents` | | `UserQueueMessage` | one per user with new RNA insights | none — then multiplies again per FCM token | Note the two paths that reach `UserQueueMessage`: Plate Processor writes it directly (step 6), and Field & Zone Service writes it again once a zone report PDF is ready (step 8). Sequence Processor is the exception — it writes notifications straight to Data Connect via `insertNotification` rather than emitting an event, which is why that branch terminates at step 7. ## Notes for maintainers `ZoneGenerateReport` is rendered as a custom node because it isn't modelled as an event in this catalog yet — Plate Processor and Field & Zone Service both reference it in their frontmatter, but there's no `events/ZoneGenerateReport` directory. Same for `ZoneReportGenerated` and `FieldAdded`. Documenting them would let this flow point at real messages and pick up their schemas. --- id: PlateScanFanout name: Plate scan fan-out version: 0.0.1 summary: | What happens when a plate is scanned into the lab: one plate scan becomes a per-sequence scan (or repeat-scan) event and a "sample received" notification per first-time sample. owners: - platform-team badges: - content: Core pipeline backgroundColor: green textColor: green steps: - id: 1 title: Plate scanned in the lab summary: | The CropDx backend writes events/plate/scan/{eventId}. The handler reads `data.payload.plate_id`; if it is missing it logs an error and stops with no fan-out. message: id: PlateScanned version: 0.0.1 next_step: id: 2 label: triggers plateScanEvent - id: 2 title: Plate Processor loads the plate and fans out summary: | `plateScanEvent` loads the plate with its sequences and users, then runs four tasks in parallel (Promise.allSettled). Per-sequence branching is decided by how many sequences the sample already has. service: id: PlateProcessor version: 0.0.1 next_steps: - id: 3 label: first sequence for the sample - id: 4 label: sample seen before (>1 sequence) - id: 5 label: per first-time sample - id: 6 label: Slack summary - id: 3 title: Sequence scanned summary: | `queueSequenceScanEvents` writes events/sequence/scan/{eventId} when this is the sample's first and only sequence (`_count == 1`). Carries `skipSlackNotification: true`. message: id: SequenceScanned version: 0.0.1 next_step: id: 7 label: triggers sequenceScanEvent - id: 4 title: Sequence repeat-scanned summary: | Same loop writes events/sequence/repeatscan/{eventId} instead when the sample already has more than one sequence (`_count > 1`). message: id: SequenceRepeatScanned version: 0.0.1 next_step: id: 8 label: triggers sequenceRepeatScanEvent (no-op while flagged off) - id: 5 title: Notify users their sample arrived summary: | `notifyUsersOfSampleReceived` queues a "Sample package received" message per user for samples with exactly one sequence. `notifyUsersOfRepeatScan` would queue a "being re-processed" message too, but only when SEND_REPEAT_SCAN_NOTIFICATION is true (it is false today). message: id: UserQueueMessage version: 0.0.1 next_step: id: 9 label: triggers userQueueMessageEvent - id: 6 title: Post to Slack summary: | `sendSlackNotification` posts the raw scan to the SLACK_PLATES_WEBHOOK_URL channel. This is the one branch that fires before any plate_id check. externalSystem: name: Slack (#plates) summary: Operational visibility channel — every plate event is echoed here. - id: 7 title: Sequence Processor — "Sample received" summary: | `sequenceScanEvent` calls `insertNotification` for each user on the grower, writing straight to Data Connect (no further event), unless the sample is older than DAYS_BACK_THRESHOLD. service: id: SequenceProcessor version: 0.0.1 - id: 8 title: Sequence Processor — repeat scan (gated off) summary: | `sequenceRepeatScanEvent` returns immediately unless SEND_REPEAT_SCAN_NOTIFICATION is true, so no notification is sent today. Documented so the wiring is not lost. service: id: SequenceProcessor version: 0.0.1 - id: 9 title: Messaging Service delivers summary: | Expands one queued message into UserSendMessage (one per FCM token) and UserSendEmail, each delivered by its own handler. service: id: MessagingService version: 0.0.1 --- ## Overview The channel view shows the CropDx backend publishing `PlateScanned` and Plate Processor consuming it — but not that a single scan can fan out into one sequence event *per sample on the plate* plus a notification per first-time sample. That branching lives inside `plateScanEvent`, and this flow makes it explicit, with the per-sequence cardinality on each edge. ## The scan-time branch For each sequence on the plate, `queueSequenceScanEvents` looks at how many sequences the *sample* already has: | Sample's sequence count | Event written | Downstream | |---|---|---| | exactly 1 (first sighting) | `SequenceScanned` | "Sample received" notification | | more than 1 (re-scan) | `SequenceRepeatScanned` | consumer no-ops while `SEND_REPEAT_SCAN_NOTIFICATION` is `false` | The user-facing "Sample package received" message is queued in parallel by `notifyUsersOfSampleReceived`, so a first-time sample produces both a `SequenceScanned` event *and* a `UserQueueMessage` — two independent paths from the same scan. ## Related flows Once sequencing finishes for this plate, see [Plate sequencing completed](/docs/flows/PlateSequencingCompleted); once predictions land, see [Plate prediction fan-out](/docs/flows/PlatePredictionFanout). --- id: PlateSequencingCompleted name: Plate sequencing completed version: 0.0.1 summary: | The BaseSpace round-trip: a sequencing-complete event enqueues a rate-limited task that pulls run stats from Illumina, writes them back as an event, and a second handler records them in Data Connect. owners: - platform-team badges: - content: Core pipeline backgroundColor: green textColor: green steps: - id: 1 title: Sequencing finished for a plate summary: | The CropDx backend writes events/plate/sequencingcomplete/{eventId}. The handler needs `data.plateId`; without it, it logs an error and stops. message: id: PlateSequencingCompleted version: 0.0.1 next_step: id: 2 label: triggers plateSequencingCompleteEvent - id: 2 title: Plate Processor enqueues the pull task summary: | `plateSequencingCompleteEvent` runs two tasks in parallel — post to Slack and enqueue the pull task. It does not call BaseSpace directly; the work is deferred onto a rate-limited queue. service: id: PlateProcessor version: 0.0.1 next_steps: - id: 3 label: enqueue processPullSequencingResultsTask - id: 7 label: Slack summary - id: 3 title: Pull sequencing results task (rate-limited) summary: | `processPullSequencingResultsTask` runs at most 1/sec with retries (5 attempts, 20s–1h backoff). Loads the plate name from Data Connect, then calls BaseSpace. custom: title: processPullSequencingResultsTask type: Cloud Tasks queue icon: ClockIcon color: blue summary: | onTaskDispatched handler. Serializes BaseSpace calls to avoid overwhelming the external API. properties: Concurrency: 1 dispatch/sec Retries: 5 attempts, 20s–3600s backoff next_step: id: 4 label: GET /v1pre3/search?scope=runs - id: 4 title: Illumina BaseSpace summary: | `pullSequencingResults` searches runs, filters to the one whose ExperimentName matches the plate, and reads SequencingStats (PercentGtQ30, PercentPf, YieldTotal, Occupancy, ...). Bails if it does not find exactly one matching run. externalSystem: name: Illumina BaseSpace summary: External sequencing platform. Source of the run quality statistics. url: https://api.basespace.illumina.com/v1pre3/search next_step: id: 5 label: writes events/plate/sequencingresults - id: 5 title: Sequencing results pulled summary: | The task writes events/plate/sequencingresults/{eventId} with the run stats and `skipSlackNotification: true`. message: id: PlateSequencingResultsPulled version: 0.0.1 next_step: id: 6 label: triggers plateSequencingResultsPulledEvent - id: 6 title: Plate Processor records the stats summary: | `plateSequencingResultsPulledEvent` calls `addPlateMetadata`, which inserts a sequencing-stat row and links it to the matching plate(s) in Data Connect via `insertSequencingStat` + `updatePlate`. Terminal — no further event. Despite `skipSlackNotification`, it still posts its own Slack summary keyed on experimentName. service: id: PlateProcessor version: 0.0.1 next_step: id: 7 label: Slack summary (own call) - id: 7 title: Post to Slack summary: | `sendSlackNotification` to the SLACK_PLATES_WEBHOOK_URL channel. Reached both from the initial handler and from the results-pulled handler's own call. externalSystem: name: Slack (#plates) summary: Operational visibility channel. --- ## Overview This is the clearest example of *one event triggering another* that the channel view can't show — because the two events aren't connected by a producer/consumer edge at all. `PlateSequencingCompleted` doesn't route to `PlateSequencingResultsPulled`. Instead the handler enqueues a Cloud Tasks job, that job calls Illumina BaseSpace, and *the job* writes the second event. The causal chain runs through a task queue and an external API — invisible on any transport diagram, explicit here. ## Why the task queue is in the middle BaseSpace calls are serialized to 1/sec (`processPullSequencingResultsTask` sets `maxDispatchesPerSecond: 1`) with aggressive retries, because a burst of direct calls overwhelmed the API. So the hop from step 2 to step 5 is deliberately asynchronous and rate-limited — a plate whose sequencing just completed may not have its stats recorded for some time if the queue is backed up. ## Related flows This runs before predictions are available; once they land, see [Plate prediction fan-out](/docs/flows/PlatePredictionFanout). --- id: RecordNotifierPattern name: Record notifier pattern version: 0.0.1 summary: | The shared shape behind ~25 simple events: a record add/edit (or telemetry) event fires, a createEventHandler consumer posts one Slack summary, and nothing else happens. Documented once here instead of per event. owners: - platform-team badges: - content: Pattern backgroundColor: gray textColor: gray steps: - id: 1 title: A record event is written summary: | The CropDx backend writes events/{domain}/{action} — an add/edit of a business record (plate, sample, grower, zone, ...) or a telemetry signal (error/success). See the table below for the full set. custom: title: Record add/edit / telemetry event type: ~25 events share this shape icon: BoltIcon color: gray summary: e.g. PlateAdded, GrowerAdded, ZoneEdited, DelphiError, ... next_step: id: 2 label: triggers the matching *Event handler - id: 2 title: createEventHandler consumer summary: | Every one of these handlers is built by the `createEventHandler` factory in `simpleEvents.ts` (allocation.ts reuses it too). It loads the document and posts a single Slack summary — the red/green colour comes from whether the title contains "error". custom: title: createEventHandler (genericEvent.ts) type: Shared factory icon: BoltIcon color: gray summary: onDocumentCreated → sendSlackNotification. No fan-out. next_step: id: 3 label: sendSlackNotification - id: 3 title: Slack (#plates) externalSystem: name: Slack (#plates) summary: Operational visibility channel. --- ## Overview Roughly 25 events do nothing but post to Slack. Rather than 25 near-identical flows, this one documents the shape and enumerates the members. If you're looking at any event in the table below, this is its entire lifecycle: written by the backend, echoed to Slack, done. No downstream events, no Data Connect writes, no user notifications. ## Events that use this pattern Each is consumed by a `createEventHandler`-built handler and only posts to Slack. | Service | Events | |---|---| | **Telemetry Notifier** (Platform) | DelphiError, DelphiSuccess, HermesError | | **Diagnostics Record Notifier** | PlateAdded, PlateEdited, SequenceAdded, SequenceEdited, SampleAdded, SampleEdited, SpectroscopyAdded, SpectroscopyEdited, HarvestAdded, HarvestEdited | | **Grower Record Notifier** | PartnerAdded, GrowerAdded, PurchaseAdded, SamplerAdded, FieldEdited, ZoneAdded, ZoneEdited | | **User Record Notifier** | UserAdded, UserEdited | | **Allocation Service** | PartnerAllocated | The events with real behaviour are documented separately: [Allocation added](/docs/flows/AllocationAdded), [Field added](/docs/flows/FieldAdded), and the plate/sequence fan-outs under Diagnostics. --- id: SmsDelivery name: SMS delivery version: 0.0.1 summary: | How events/user/sendsms is delivered via Vonage — and why the path is inert today: nothing produces the event, and the recipient is hard-coded. owners: - platform-team badges: - content: No producer backgroundColor: orange textColor: orange steps: - id: 1 title: SMS requested summary: | A document at events/user/sendsms/{eventId} with `phone` and `body` would trigger delivery. No handler currently writes this — the queuemessage fan-out emits UserSendMessage and UserSendEmail, but not UserSendSMS. message: id: UserSendSMS version: 0.0.1 next_step: id: 2 label: would trigger userSendSMSEvent - id: 2 title: Messaging Service sends the SMS summary: | Posts to Slack (unless skipped), then sends via Vonage — but only in the `delphi-1f2b6` project and the non-local env. Returns early otherwise. service: id: MessagingService version: 0.0.1 next_step: id: 3 label: vonage.messages.send - id: 3 title: Vonage summary: | SMS provider. Today the recipient is hard-coded to a single test number (447866845818) and `data.phone` is ignored. externalSystem: name: Vonage summary: SMS delivery provider. --- ## Overview Documented so the wiring isn't lost, but this path is **inert in practice**. Two things stop it: no handler produces `UserSendSMS` (so step 1 never fires naturally), and even when invoked the consumer ignores `data.phone` and sends to a hard-coded test number, only in the `delphi-1f2b6` project. Treat it as a stub to finish, not a working channel — contrast with UserSendMessage / UserSendEmail in the [MessagingService](/docs/services/MessagingService) graph, which are produced and delivered for real. --- id: ZoneReportGeneration name: Zone report generation version: 0.0.1 summary: | From a report request to a delivered PDF: a generate-report event enqueues a rate-limited task that renders the PDF via a Cloud Run service, writes a report-generated event, and notifies subscribers, grower and partner. owners: - platform-team badges: - content: Reports backgroundColor: green textColor: green steps: - id: 1 title: Report requested for a zone summary: | events/zone/generatereport/{eventId} is written upstream — by Plate Processor's `queueZoneReportEvents` (one per non-dropped zone whose grower is in the hard-coded subscription list) and, on a test-only path, by Sequence Processor. Not modelled as an event in this catalog yet. custom: title: ZoneGenerateReport type: Event (undocumented) icon: BoltIcon color: orange summary: | Firestore path events/zone/generatereport/{eventId}. Carries fieldId, zoneId, sampleId, growerId, allocationId, subscribers, messageGrower, messagePartner. next_step: id: 2 label: triggers zoneGenerateReportEvent - id: 2 title: Field & Zone Service enqueues the render task summary: | `zoneGenerateReportEvent` requires growerId, sampleId, fieldId, fieldName, zoneId and zoneName in the payload; if any are missing it logs and skips. Otherwise it enqueues the rate-limited render task. service: id: FieldZoneService version: 0.0.1 next_step: id: 3 label: enqueue processGenerateZoneReportTask - id: 3 title: Generate zone report task (rate-limited) summary: | `processGenerateZoneReportTask` runs at most 1/sec with retries. This serialization exists because a burst of parallel requests made the Cloud Run service return HTML error pages instead of JSON. custom: title: processGenerateZoneReportTask type: Cloud Tasks queue icon: ClockIcon color: blue summary: | onTaskDispatched handler. Serializes PDF renders so download-pdf-report is not overwhelmed. properties: Concurrency: 1 dispatch/sec Retries: 5 attempts, 20s–3600s backoff next_step: id: 4 label: POST /generate (mints a custom auth token) - id: 4 title: download-pdf-report Cloud Run service summary: | `requestZoneReport` mints a `cloud-functions-service` custom token, POSTs {fieldId, sampleId, zoneId, token}, and renders the report. A non-OK response throws so the task queue retries rather than parsing an HTML error page. externalSystem: name: download-pdf-report (Cloud Run) summary: External service that renders the zone report PDF. url: https://download-pdf-report-1030831003496.us-central1.run.app/generate next_step: id: 5 label: on success, writes events/zone/reportgenerated - id: 5 title: Report generated summary: | On a successful render, `generateAndStoreZoneReport` writes events/zone/reportgenerated/{eventId} with the pdfUrl and the messageGrower / messagePartner / subscribers routing flags. Not modelled as an event in this catalog yet. custom: title: ZoneReportGenerated type: Event (undocumented) icon: BoltIcon color: orange summary: | Firestore path events/zone/reportgenerated/{eventId}. The service both produces and consumes this event. next_step: id: 6 label: triggers zoneReportGeneratedEvent - id: 6 title: Field & Zone Service notifies recipients summary: | `zoneReportGeneratedEvent` builds the recipient set from the payload — explicit `subscribers`, plus grower users when `messageGrower`, plus partner users when `messagePartner` — deduplicates it, and queues a "Report ready" message with the PDF attached. Also posts to Slack. service: id: FieldZoneService version: 0.0.1 next_step: id: 7 label: one per deduplicated recipient - id: 7 title: Notify recipients summary: | `notifyUsersOfNewReport` queues a "Report ready for field ... (zone ...)" message per recipient, carrying `attachmentUrl` = the PDF link. message: id: UserQueueMessage version: 0.0.1 next_step: id: 8 label: triggers userQueueMessageEvent - id: 8 title: Messaging Service delivers summary: | Expands one queued message into UserSendMessage (one per FCM token) and UserSendEmail, each delivered by its own handler. service: id: MessagingService version: 0.0.1 --- ## Overview The report pipeline is a chain of events joined by things the transport view never shows: a rate-limited task queue and an external Cloud Run render service. `ZoneGenerateReport` does not route to `ZoneReportGenerated`; the render task produces the second event only *after* the PDF succeeds. And Field & Zone Service both writes and reads `ZoneReportGenerated` — it is its own downstream consumer, a self-loop that a producer/consumer diagram flattens into noise. ## Who gets the report The routing flags set by the upstream producer decide the recipient set at step 6: | Flag / field | Adds to recipients | |---|---| | `subscribers` | the explicit userIds on the request | | `messageGrower` | every user on the grower | | `messagePartner` | every user on the allocation's partner | The set is deduplicated before messages are queued, so a user who is both a subscriber and a grower user is notified once. ## Related flows The request that starts this flow is produced by [Plate prediction fan-out](/docs/flows/PlatePredictionFanout) (step 5). `ZoneGenerateReport` and `ZoneReportGenerated` are rendered as custom nodes because they are not yet modelled as events in this catalog — documenting them under Field & Zone Service would let both flows point at real messages and pick up their schemas.