lumnik. / scenarios All scenarios · Français

SENAITE: Fleming's plate waiting to be verified

Goal Be told when a SENAITE LIMS result goes over its limit, then when nobody verifies it.

A real SENAITE 2.6, five research groups, 120 culture plates read through its JSON API.

manifests/lab-docker-compose.yml
# SENAITE LIMS 2.6.0, the official image, with an empty site: the runner seeds it through its own API.name: lumnik-senaite-platesservices:  senaite:    image: senaite/senaite:v2.6.0-docker2    platform: linux/amd64                               # the only image SENAITE publishes    environment:      SITE: senaite      PASSWORD: ${SENAITE_PASSWORD:?the runner sets a throwaway admin password}    ports: ["127.0.0.1:${SENAITE_PORT:-9390}:8080"]     # this machine only
manifests/10-plates.yaml
apiVersion: connectors.lumnik.io/v1kind: Connectormetadata:  name: scenario-senaite-a  connector_type: rest-generic  scopes: [laboratory]spec:  base_url: http://host.docker.internal:9390/senaite/@@API/senaite/v1  auth:    kind: basic    username: admin    password_env: SCENARIO_SENAITE_A_PASSWORD  endpoints:    - id: analyses      path: /analysis?complete=yes                     # complete=yes: the result itself, not only the catalog's summary      method: GET      pagination:        kind: body-link        link_path: $.next                              # SENAITE writes the next page's URL, b_start=25, 50…        response_path: $.items        total_path: $.count                            # and how many analyses the list holds      target:        kind: hybrid        table: ext.scenario_senaite_a        unique_key: $.uid        promote:          - { name: sample, path: $.getRequestID, type: text }          - { name: research_group, path: $.getClientTitle, type: text }          - { name: analysis, path: $.getKeyword, type: text }          - { name: result, path: $.Result, type: decimal }          - { name: unit, path: $.getUnit, type: text }          - { name: review_state, path: $.review_state, type: text }          - { name: sampled_at, path: $.getDateSampled, type: timestamptz }
manifests/20-plate-over-limit.yaml
apiVersion: apps.lumnik.io/v1kind: Processmetadata:  name: scenario-senaite-over-limit-aspec:  scope: laboratory  on:    event: data.row.changed    table: ext.scenario_senaite_a                      # the REST connector's own table (icreated/lumnik#1816)  when:    - result>100                                       # a clean plate holds at most 100 colonies  emit:    type: decision.plate-over-limit-a    key: sample                                        # the inbox shows the sample, not the row hash (icreated/lumnik#1823)    message: "{sample} ({research_group}): {result} {unit}, a clean plate holds at most 100 — state {review_state}"
$ lm connector run scenario-senaite-c
run scheduled: fe25ee34-e7f8-4b53-9ad1-e3c04afc3d9e

$ lm run get fe25ee34-e7f8-4b53-9ad1-e3c04afc3d9e
ID        CONNECTOR           ENDPOINT  STATUS     IN   OUT  SKIPPED  DEAD-LETTERS  STARTED           ENDED             EVALUATED
fe25ee34  scenario-senaite-c  analyses  Completed  120  120  0        0             2026-11-04 23:58  2026-11-04 23:58  yes

$ lm run get fe25ee34-e7f8-4b53-9ad1-e3c04afc3d9e --wait-evaluated --timeout 2m
ID        CONNECTOR           ENDPOINT  STATUS     IN   OUT  SKIPPED  DEAD-LETTERS  STARTED           ENDED             EVALUATED
fe25ee34  scenario-senaite-c  analyses  Completed  120  120  0        0             2026-11-04 23:58  2026-11-04 23:58  yes

$ lm inbox --process scenario-senaite-over-limit-c
ID     WHEN      PROCESS                        MESSAGE                                                                                                     ENTITY_KEY
23570  just now  scenario-senaite-over-limit-c  analysisrequest-24 (Fleming group): 2400 CFU/plate, a clean plate holds at most 100 — state to_be_verified  analysisrequest-24
manifests/30-plate-review.yaml
apiVersion: apps.lumnik.io/v1kind: Workflowmetadata:  name: scenario-senaite-review-aspec:  scope: laboratory  provenance: declared  on:    table: ext.scenario_senaite_a    column: review_state                               # SENAITE's own state, as its API writes it    key: sample  dormancy:    after: 1m                                          # a lab would write 2d; shortened for the replay    message: "{doc_key} ({research_group}): {result} {unit}, {state_label} since {since}"  states:    - { code: unassigned, label: "Received, not counted", initial: true }    - { code: to_be_verified, label: "Waiting for verification" }    - { code: verified, label: "Verified", terminal: true }

Limits

  • The dormancy counts from lumnik's first sighting of the state, not from SENAITE's submission date.
  • The message's {since} is a UTC date: a plate seen at 00:09 Paris time on 5 November reads "since 2026-11-04".
  • body-link sends no size: SENAITE's 25 per page governs the run. Put limit= in path: to change it.
  • Fictional groups and plates, a nod to their namesakes; synthetic data.

Laboratory · microbiology

The results live in SENAITE.

Someone must be told of a result over its limit.

SENAITE 2.6, 120 culture plates

The problem

Fleming's plate counts 2400 colonies.

  • A clean plate holds at most 100.
  • The result waits to be verified.

What is not changed

SENAITE stays as it is.

  • Its JSON API, as it is.
  • No add-on, no export.

What had to be written

when: [ result>100 ]
key: sample

A Process on the REST table.

  • A decision when the plate goes over 100.
  • A Workflow when it sleeps unverified.

What was fixed

Two gaps found, two fixes.

  • A REST source feeds a Process (#1816).
  • The decision carries the sample's name (#1823).

Try it

The scenario replays locally.

Every step is published with its output.

This scenario runs on lumnik Open, free and open source.

Try lumnik Open →