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 itprocessStatusId/processStatusLabel— which activity (node) in the graph the record currently sits atinstanceId— the record’s own_id, used as the workflow instance identifier
Graph vocabulary
| Shape / color | Meaning |
|---|---|
| Grey rounded rectangle | A state activity — the record sits here after an action runs; has a numeric ID + label (e.g. 102: - created log entry) |
| Cyan diamond | A switch (if/else) — evaluates a condition against the record’s fields and routes to one of two (or more) next activities |
| Cream/yellow rounded rectangle | A state that is a waiting point for external action — e.g. 317: n8n - automated operations - not human actions |
| White oval/rounded shape | A button — a manual trigger a caller must explicitly invoke (via API or UI) to advance the record past this point |
Black arrow, label auto | Automatic transition — the engine advances the record through this immediately, no external action needed |
Blue arrow, label manual | Manual transition — requires an explicit button click (API call) to advance; the engine will NOT auto-advance past this |
Green arrow, label trigger | A 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>®Id=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.
Step 3 — Link the file to the record
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 viaGET /api/offices/files/:fileId/getBase64File, then builds the preview blob withb64toBlob(res.base64, 'application/pdf', null)— the MIME type is hardcoded toapplication/pdfclient-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) — fetchesGET /api/offices/files/:fileId/getSignedUrland points an iframe/embed directly at the resulting S3 URL. This method does depend on the real Content-Type:res.ContentTypedrivesgetFileTypeByMime(), 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 ContentType | application/pdf | application/octet-stream |
response-content-disposition | inline | inline (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-bug → feature/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 exposure | Only 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 backing | Same exposure the real web app already has — nothing new |
| Expiry | Doesn’t expire until manually rotated | ~8 hours (iat/exp in the token) — needs periodic refresh for a recurring automation |
| Attribution | Generic/default creator | Tied to the real logged-in user — every automated record shows as that person having created it |
| Scope | Single shared secret, same value for every “automatedRequest: true, type: integration” caller | Per-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-v2compose service,container_name: s3-v2-v42-prod, mirroring the persistent-patch bind-mount pattern already used forv42-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 thefileData.mimetypefix above,storage.config.js,app.routing.js) andinfra-src/pinbox24/w4/s3-v2-patches/(mailgunFileHandler.helper.js) .github/workflows/secrets-sync.yml:s3-v2-v42-prodadded to the “Deploy W4 containers” step’s stop/rm/recreate list (previously onlybackend,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-prodstep that previously ran after container deploy to pick up a freshly-syncedMONGODB_URI(#3235) — recreation viadocker-compose up -d s3-v2already 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.
Related
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 specificallydocs/pinbox24/ai-processing-register-workflow.md— earlier architecture notes on the AI-Logs process