---
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.