Pinbox24 W4 — Workflow (Process) Engine

Status: Living document — started 2026-07-11 during the n8n direct-API integration work. Extend as more of the engine is mapped.

What this is

Every register in Pinbox24 (e.g. the AI-Logs register, regId: 67597cb44eefb1002c9847d4) is driven by a process definition — a visual workflow graph of activities and transitions, stored in w4_db.process.configs (per earlier sessions’ references) and executed by an internal workflow engine whenever a record (regRecords document) is created or advanced.

Every record has:

  • processId — which process definition governs it
  • processStatusId / processStatusLabel — which activity (node) in the graph the record currently sits at
  • instanceId — the record’s own _id, used as the workflow instance identifier

Graph vocabulary

Shape / colorMeaning
Grey rounded rectangleA state activity — the record sits here after an action runs; has a numeric ID + label (e.g. 102: - created log entry)
Cyan diamondA switch (if/else) — evaluates a condition against the record’s fields and routes to one of two (or more) next activities
Cream/yellow rounded rectangleA state that is a waiting point for external action — e.g. 317: n8n - automated operations - not human actions
White oval/rounded shapeA button — a manual trigger a caller must explicitly invoke (via API or UI) to advance the record past this point
Black arrow, label autoAutomatic transition — the engine advances the record through this immediately, no external action needed
Blue arrow, label manualManual transition — requires an explicit button click (API call) to advance; the engine will NOT auto-advance past this
Green arrow, label triggerA transition fired by an explicit trigger call (as opposed to the default auto-advance)

AI-Logs register flow (regId 67597cb44eefb1002c9847d4) — as mapped so far

Start
  → 102: created log entry (auto)
  → 314: switch — creationSource = n8n ? (auto)
       NO  → 315: no n8n log creation (auto)
              → 308: switch — avoidDupCheck (existing legacy email-integration path,
                     see docs/playbooks/pinbox24-mailgun-duplicate-check-fix.md
                     activities 296-300 for the duplicate-check sub-flow this leads into)
       YES → 316: n8n log creation (auto)
              → 317: n8n - automated operations - not human actions  ← WAITING STATE
                     (manual transition only — engine stops here)
                → 318: go forward to duplicacy file check  ← BUTTON
                     (click required to advance; rejoins the common downstream flow
                     that duplicate-check / AI processing already uses)

Key design point: creationSource is a field on the record (set at creation time in the POST /api/reg/:regid/new body) that the process engine’s activity 314 switch reads to decide which path a new record takes. Setting creationSource: "n8n" diverts the record to the n8n-specific waiting state (317) instead of the default email-integration path (315).

The n8n-sourced record lifecycle — VERIFIED END-TO-END 2026-07-11

Unlike the direct email-integration flow (which uploads the file and creates the record with recordMainDocument already set in one shot — see pinbox24-mailgun-duplicate-check-fix.md for why that ordering matters there), an n8n-sourced record follows a 4-step lifecycle. Confirmed live end-to-end on aiLog/2026/7/11/4 (_id: 6a51f47a7b5a05003f07066a): created → 317 → file uploaded → linked → button clicked → duplicate-check ran correctly (duplicateCount: 1, duplicate: "N", self-match only) → advanced to processStatusId: 71 | "Document passed to docRegistration"aiProcInfo reached docs_ai.ai_response_ready. Same terminal path as a normal email-sourced record.

Step 1 — Create the record with creationSource: "n8n", no file yet

POST /api/reg/67597cb44eefb1002c9847d4/new
{
  "officeId": "5c752bbca20da35ca1b99083",
  "aiProc": "pdf",
  "aiProcInfo": "docs_ai.waitingForProcessing",
  "costCategory": "Rechnung",
  "ai-email-address": "rechnung-AI@integrations-eu.pinbox24.com",
  "creationSource": "n8n",
  "summary": "...", "desc": "...", "emailBodyText": "...",
  "emailAdresat": "...", "reporter": "..."
}

→ record lands at processStatusId: 317 | "n8n - automated operations - not human actions". Confirm via a follow-up GET after a short delay (the switch evaluation is not instant).

Step 2 — Upload the file, tied to the record via query params

POST /api/offices/files/upload?officeId=<officeId>&recId=<record _id>&regId=67597cb44eefb1002c9847d4&creatorId=<userId>&creatorLogin=<userLogin>
multipart/form-data, field name: uplFile

→ returns a file doc with its own _id. Files are stored in Wasabi, replicated across 3 buckets/regions automatically (p24-was-us-east-1, plus two cross-region replicas).

Note: creatorId/creatorLogin are required query params on this endpoint (captured from a real UI session) — for a service-account-based caller these would be that account’s own IDs.

PUT /api/reg/67597cb44eefb1002c9847d4/<record _id>
{ "recordMainDocument": "<file _id>", "originalName": "<filename>", "fileSize": "<bytes, as string>" }

This does not re-trigger duplicate-check on its own — the record stays at 317, which is correct, since 317 is a manual-transition waiting state, not an auto-advancing one.

Step 4 — Click the process button (318) to advance out of the waiting state

GET /api/reg-process/<record _id>/<activityId>?comment=null

No body, no -X needed (plain GET). activityId here is 318 (the specific button node in the AI-Logs process graph — see the flow diagram above). This is the same endpoint the UI’s button click fires — captured live from a real browser session. Triggers the engine to run duplicate-check and continue into the common AI-processing flow.

Auth note

All 4 calls above were tested using a real user JWT session token (Authorization: <JWT>, plus officeid header). This is the same “front-door” auth the web UI itself uses — no static integration token, no internal-network requirement. See “Auth architecture options” below for the tradeoffs vs. the static-token approach mailgun-v42-prod uses internally.

The duplicate UI field is not what it looks like

The record-detail UI has a form field labeled “duplicate” that displays raw HTML with ##placeholder## merge tags (##originalName##, ##fileSize##, ##duplicate##, ##duplicateCount##). This is not the real recordData.duplicate Y/N flag — it’s a templated display column using Pinbox24’s own merge-tag templating system, apparently bound to the wrong form field key (or reused across multiple purposes). If you save the record via the UI’s “Save and back” before duplicate-check has run, the template gets persisted mid-substitution (some placeholders resolved, others still raw) — this is a pre-existing app-level form-config quirk, not something introduced by API-based record creation. The actual recordData.duplicate / recordData.duplicateCount fields are separate and behave as documented in pinbox24-mailgun-s3v2-stabilization.md.

Bug: files uploaded via /api/offices/files/upload download instead of preview — FIXED, MR open

Confirmed and root-caused 2026-07-11. Files attached via POST /api/offices/files/upload (the endpoint the UI’s own file-attach button calls) end up with the wrong Content-Type on the actual S3/Wasabi object, causing the browser to download them instead of rendering an inline preview.

Two preview methods, and why this only breaks one of them

The frontend (pinbox24-version-4/src/.../file-preview.component.ts) supports two preview methods, chosen per form field definition (field.requestType, not per-record — cannot be toggled per file from the UI):

  • getDocument() (base64 method) — fetches via GET /api/offices/files/:fileId/getBase64File, then builds the preview blob with b64toBlob(res.base64, 'application/pdf', null) — the MIME type is hardcoded to application/pdf client-side, so this method works regardless of what Content-Type is actually stored on the S3 object. This is the older/legacy preview method.
  • getDocumentByUrl() (signed-URL method) — fetches GET /api/offices/files/:fileId/getSignedUrl and points an iframe/embed directly at the resulting S3 URL. This method does depend on the real Content-Type: res.ContentType drives getFileTypeByMime(), and more importantly the browser itself decides preview-vs-download based on the actual HTTP Content-Type header it gets back from S3 when loading that URL. This is the current/preferred method — used for email/mailgun- sourced files.

Confirmed via GET /api/offices/files/:fileId/getSignedUrl (returns real S3 object metadata):

Working file (uploaded via mailgunFileHandler)File uploaded via /api/offices/files/upload
S3 object ContentTypeapplication/pdfapplication/octet-stream
response-content-dispositioninlineinline (same, correct on both)
MongoDB files.mimeType(not stored on this doc)application/pdf (correct)
MongoDB files.type"registries""Cases"

Root cause — confirmed in source

pinbox24-ms-s3-v2/src/apps/storage/storage.controller.ts, upload() handler:

// req.files.forEach((fileData: any) => { ... })
doc = await uploadMultiFiles(key, fileData, fileData.type, doc._id);

fileData is a multer file object — it has .mimetype, .originalname, .buffer, .size, but no .type property. fileData.type is therefore always undefined. uploadMultiFiles passes that straight through as the S3 PutObject ContentType parameter (storage.helper.ts:56), which silently defaults to application/octet-stream when undefined — for every file ever uploaded through this endpoint, app-wide, regardless of office or register.

This is a copy/paste mix-up, not a deliberate design choice: req.body.type (a different variable, holding the storage-category string like "Cases"/"registries") is set one line above (line 117) — someone grabbed the wrong .type. The two sibling call sites for the same uploadMultiFiles helper (mailgunFileHandler.helper.ts, scanque.controller.ts) already use fileData.mimeType correctly for their own custom-built attachment objects, and storage.helper.ts’s own makeFileObject (same file as the bug, two lines below) already reads fileData.mimetype correctly for this exact fileData object shape — confirming the fix.

Fix — MR open

- doc = await uploadMultiFiles(key, fileData, fileData.type, doc._id);
+ doc = await uploadMultiFiles(key, fileData, fileData.mimetype, doc._id);

MR: https://gitlab.com/pinbox24/pinbox24-ms-s3-v2/-/merge_requests/11 (branch fix/upload-content-type-bugfeature/scanque, the repo’s default branch)

fileData.type was always undefined before this fix — no existing behavior depended on it, so this can only improve correctness. Low code risk; test on staging (s3-v2-test.dev.pinbox24.com) before prod purely because this is a shared, business-critical service used app-wide, not because of anything specific to this change.

The type: "Cases" vs "registries" difference on the MongoDB files document is a separate, unrelated field (storage-category classification, not Content-Type) — not addressed by this fix, not currently known to cause any problem.

Auth architecture options — static token vs. JWT session

Two working auth methods were verified for these endpoints:

Static AUTH_TOKEN (mailgun-v42-prod’s method)JWT session token
Network exposureOnly ever used internally (http://v42-prod:3000, same Docker network) — using it from an external server like bms-4 exposes it to the open internet with no network-layer backingSame exposure the real web app already has — nothing new
ExpiryDoesn’t expire until manually rotated~8 hours (iat/exp in the token) — needs periodic refresh for a recurring automation
AttributionGeneric/default creatorTied to the real logged-in user — every automated record shows as that person having created it
ScopeSingle shared secret, same value for every “automatedRequest: true, type: integration” callerPer-user session

Decision (2026-07-11): proceed with JWT-based auth for n8n → Pinbox24 direct-API calls, to avoid opening new network-layer exposure. Attribution-to-a-human-account tradeoff noted as a follow-up — mirrors the existing plan to provision a dedicated automation account (et-n8n@..., see docs/priorities.md) rather than use a personal login for bulk automated writes long-term.

Incident: s3-v2-v42-prod had no compose management — near-outage during the content-type hotfix

While applying the live hotfix above (fileData.mimetype), the assumption was that docker-compose up -d --force-recreate s3 in p4-back-ts/docker-compose.yml would recreate s3-v2-v42-prod. It does not — the s3 service in that compose file is the legacy v4-s3 image, container name s3-v42-prod (no “v2”). s3-v2-v42-prod (the actual target, pinbox24-ms-s3-v2 repo, the container s3-v2-api.w4.pinbox24.com routes to) had no compose service at all — it was a standalone container from an earlier manual docker run, invisible to docker-compose.yml and to git.

Sequence: docker stop + docker rename on s3-v2-v42-prod (expecting docker-compose up -d s3 to replace it) → the compose command instead recreated the unrelated s3-v42-prod container → s3-v2-v42-prod was left with no running container at all, on a business-critical, app-wide upload service. A local safety hook correctly blocked a silent revert (renaming the backup back and starting it) since that wasn’t an explicitly authorized rollback action. Recovery: extracted the exact original container config (image, networks, restart policy, mounts, full env values — handled entirely server-side, never exposed in chat) and reconstructed via docker run + docker network connect, confirmed healthy, then got explicit user confirmation before any further changes.

Fix — s3-v2-v42-prod formalized into infra-src/pinbox24/w4/docker-compose.yml (2026-07-11, PR https://github.com/radieu/p24-infra/pull/3704):

  • New s3-v2 compose service, container_name: s3-v2-v42-prod, mirroring the persistent-patch bind-mount pattern already used for v42-prod (read-only mounts over the compiled JS files, avoiding an image rebuild for hotfixes)
  • Patch files that previously existed only on bms-1 (hand-edited over SSH, untracked) pulled into git: infra-src/pinbox24/w4/s3-v2-persistent-patches/ (storage.controller.js — includes the fileData.mimetype fix above, storage.config.js, app.routing.js) and infra-src/pinbox24/w4/s3-v2-patches/ (mailgunFileHandler.helper.js)
  • .github/workflows/secrets-sync.yml: s3-v2-v42-prod added to the “Deploy W4 containers” step’s stop/rm/recreate list (previously only backend, s3, redis, notify, pdf-gen, git-deploy), and a new “Deploy s3-v2-v42-prod patches to bms-1” step scp’s the two patch directories to /root/s3v2-prod/{patches,persistent-patches}/ on every sync
  • Removed the now-redundant standalone docker restart s3-v2-v42-prod step that previously ran after container deploy to pick up a freshly-synced MONGODB_URI (#3235) — recreation via docker-compose up -d s3-v2 already loads the current env file, so the extra restart was dead weight once the container is compose-managed

Applied live and verified: full stop → rename → docker-compose up -d --no-deps s3-v2 recreated the container cleanly, all 4 PM2 workers online, upload + getSignedUrl round-trip confirmed returning the correct ContentType.

Root cause class, not fully closed: this is the second incident this session caused by a service existing in production with no compose/git representation (see the mailgun stale-credential incident, docs/playbooks/mailgun-mongodb-stale-credential-hang.md, for the first). No systematic audit has been done yet for other undocumented docker run containers on bms-1/bms-3/bms-4 — this fix closes the s3-v2-v42-prod instance specifically, not the class of problem.

  • docs/playbooks/pinbox24-mailgun-duplicate-check-fix.md — the legacy email-integration path’s duplicate-check sub-flow (activities 296-300), and why file-before-record-creation ordering matters there specifically
  • docs/pinbox24/ai-processing-register-workflow.md — earlier architecture notes on the AI-Logs process