The platform is split into three n8n workflows over a shared Microsoft 365 state layer:
- Home Tab workflow - handles the Slack
app_home_openedevent. Reads the users table, finds the requesting user, builds a role-specific block list, publishes viaviews.publish. - Interaction workflow - receives Slack interactions (button
clicks, modal submissions). Runs the authorization layer first,
then routes by
action_idorcallback_idinto the matching branch (new Einwilligung, user CRUD, search, reports, details). - DocuSign Connect workflow - receives DocuSign webhook events. Verifies the HMAC SHA256 signature, parses the envelope ID, finds the matching Excel row, downloads the signed PDF, uploads to SharePoint, updates the case status, notifies Slack.
Three workflows because the trigger types are completely different (one Slack events stream, one Slack interactions URL, one DocuSign Connect URL) and the latency requirements differ. Splitting them keeps each one focused and lets one slow path not block the others.
n8n lets you cram any number of triggers into a single workflow. For this platform, that would mean one canvas with the home-tab logic, the interaction router, and the DocuSign processor all tangled together. A few reasons we don't:
- Different SLA per trigger type. When a user opens the Home tab, they want to see their dashboard within ~500ms. When DocuSign fires a completion event, latency doesn't matter as long as we acknowledge the webhook quickly. Mixing them risks one slow path affecting the other's perceived responsiveness.
- Different deploy cadence. The interaction workflow grows fast (new actions, new modals); the Home Tab workflow is mostly stable; the DocuSign workflow rarely changes. Splitting lets each evolve at its own rate without retesting unrelated parts.
- Easier failure isolation. A bug in the user-CRUD branch can't brick the Home Tab. A change to HMAC verification can't break Slack interactions.
The interaction workflow itself has 40 Code nodes covering many
action types. We could split it further: one workflow per action.
We don't, for the reason documented in
Valentino-Veljanovski/internal-reclamation-case-management-snippets:
the pre-routing prefix (parse Slack payload → check user → detect
trigger type) is shared by every action, and duplicating it per
workflow is worse than keeping one large workflow with clear node
naming.
The escape hatch for the next size jump is executeWorkflow: keep
the parser and dispatcher in the router, move each action's logic
into its own called workflow.
Slack fires app_home_opened whenever a user opens the bot's home
tab. The event payload contains the requesting user's Slack ID.
- Extract user ID from the event.
- Read the users table from the Excel workbook (Microsoft Graph).
- Find the row where
slack_idmatches. - Branch on user state:
- Not found → render an "access denied, contact admin" block.
- Inactive → render an "account deactivated" block.
- Active → branch on role and render role-appropriate blocks.
views.publishto update the home tab.
- Super admin sees everything: dashboard, all quick actions, user management entry point, compliance center, system settings.
- Admin sees the dashboard and most quick actions, but no user management.
- Technician (Techniker) sees only scan-related actions.
The role-checking pattern is documented in
role-based-app-home-pattern.md.
A single Slack interactions URL receives all interactive events:
button clicks, modal submissions, view-closed events, slash commands.
The payload field arrives URL-encoded as JSON.
Before any action runs, an authorization node:
- Parses the payload and extracts the user identity from one of
four shapes (
block_actions,view_submission,event,slash_command). The same user ID lives in different paths depending on the event type. - Determines the required permission for this action (
view,create,delete). - Short-circuits with
authorized: truefor hard-coded super-admins; otherwise checks channel membership. - If unauthorized, posts a polite "no access" response and stops.
The four-source extraction pattern is in
multi-source-authorization.md.
After authorization, a chain of switches routes by event type + callback ID + action ID:
view_submission+einwilligung_submit→ create new caseview_submission+create_user_submit→ create new user rowview_submission+delete_user_submit→ deactivate userblock_actions+action_id starts with create_→ open create modal (new Einwilligung, new user, etc.)block_actions+action_idview_users→ open users-list modalblock_actions+action_idcase_details_*→ open case-details modal- ... and so on for ~10 more branches
Each branch reads from or writes to the Excel state, builds a Slack response, and acknowledges the webhook within Slack's 3-second window.
A webhook URL configured in DocuSign Connect. DocuSign calls it on
envelope status changes. The events that matter for this platform
are envelope-completed and recipient-completed.
- Extract the HMAC signature header
(
x-docusign-signature-1, with case-insensitive lookups). - Compute HMAC SHA256 of the raw request body using the shared secret. Compare to the header value. If mismatched, abort.
- Parse the JSON body (DocuSign Connect v2.1 format) to extract
envelopeId,status,completedDateTime. - Read the Excel cases table.
- Find the row whose
docusignEnvelopeIdcolumn matches. - Download the signed PDF from DocuSign.
- Upload to SharePoint at a deterministic path derived from the
case data (e.g.
BauScope/Einwilligungen/<year>/<serial>.pdf). - PATCH the Excel row to set
docusignCompletedDate,fmptsStatus = 'completed'. - Post a Slack DM to the case creator: "✅ Signed. Scan can start."
The HMAC pattern is documented in
docusign-hmac-verification.md.
Each row is one case. Columns (in order):
0 entryId (auto-incrementing integer)
1 fmptsNumber (e.g. "FS-0161" - display string)
2 fmptsItemId (SharePoint list item ID - source of truth)
3 createdDate (Excel serial date)
4 createdBy (Slack handle of creator)
5 objektStrasse (object street)
6 objektOrt (object city)
7 objektAdresse (concatenated full address)
8 kundenname (customer name)
9 kundenEmail (customer email - for DocuSign delivery)
10 kundenTelefon (customer phone)
11 interneNotiz (internal note from creator)
12 docusignSentDate (when envelope was sent)
13 docusignCompletedDate (when envelope was signed)
14 docusignEnvelopeId (DocuSign GUID - primary key for webhook lookup)
15 fmptsStatus ('pending' | 'sent' | 'completed' | 'cancelled')
Hardcoded indexes (row.values[0][14] etc.) are fragile when columns
get added or reordered. The
Excel column mapper pattern gives
a name-based access layer over the raw indexes.
Each row is one user. Columns: index, name, email, slack_id, slack_handle, (reserved), role, active, created_date, created_by, (reserved), (reserved), description, scan_count, einwilligung_count, permission_score.
The role column is the platform's primary access-control axis.
The next serial is derived as max(existing IDs) + 1, padded to four
digits, prefixed with FS-. The number is computed in n8n at modal-
open time (so the user sees their next serial in the modal) but
committed to a SharePoint list on form submission, which acts as
the durable source of truth.
This means: if two users open the same modal at the exact same
moment, both see the same number; whichever submits first commits;
the second submission gets a duplicate-key error from SharePoint and
needs to retry. For the team's volume (single-digit cases per day)
this hasn't happened. For higher concurrency, replace with a server-
side increment that returns the new ID atomically (e.g. SharePoint
list's auto-numbering with a POST that returns the assigned ID).
Signed Einwilligung PDFs are stored at a deterministic path:
BauScope/Einwilligungen/<year>/<serial>.pdf
Path is built from the case data, not from DocuSign's response - so the path is predictable from the case ID alone. Useful for later auditing without having to query DocuSign for every envelope.
- Role-based App Home rendering →
role-based-app-home-pattern.md - DocuSign Connect HMAC →
docusign-hmac-verification.md - Type-safe Excel access →
excel-column-mapper-pattern.md - Multi-source Slack auth →
multi-source-authorization.md - Code snippets →
../snippets/README.md