n8n: store cleaned workflow definitions as repo source of truth
Move the two live-validated workflows into n8n/workflows/ (credential- based auth referenced by name only, no secret values), add a manifest covering all 4 canonical workflows and a read-only drift-check script against n8n's Public API. Retire the pre-integration root-level starter files that still carried the literal-token pattern, and repoint the Unraid deploy scripts, Makefile targets and runbook at the new files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
59cb4c062e
commit
e0c107a94a
+4
-1
@@ -60,7 +60,10 @@
|
||||
- `knowledge/procedures/09-booking-conflicts.md`
|
||||
- `knowledge/procedures/10-roles-and-escalation.md`
|
||||
- `n8n/README.md`
|
||||
- `n8n/mobilityops-return-processing.json`
|
||||
- `n8n/workflows/MANIFEST.md`
|
||||
- `n8n/workflows/fleet-ops-vehicle-return.json`
|
||||
- `n8n/workflows/fleet-ops-data-quality-scan.json`
|
||||
- `n8n/workflows/check_drift.py`
|
||||
- `seed/README.md`
|
||||
- `seed/bookings.csv`
|
||||
- `seed/customers.csv`
|
||||
|
||||
@@ -26,16 +26,19 @@ reset:
|
||||
# One-time per environment: imports and activates the n8n return-processing workflow.
|
||||
# The n8n owner account itself cannot be scripted safely and must be created once at
|
||||
# http://localhost:5678/setup (any email/password, no verification required) before
|
||||
# this target's activation takes effect. See docs/17-runbook.md.
|
||||
# this target's activation takes effect. The workflow also needs the "Fleet Ops Webhook
|
||||
# Trigger Token" and "Fleet Ops Service Token" Header Auth credentials created manually in
|
||||
# the n8n UI before it will actually process a return -- see docs/17-runbook.md.
|
||||
n8n-setup:
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-return-processing.json
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/workflows/fleet-ops-vehicle-return.json
|
||||
docker compose exec n8n n8n publish:workflow --id=mobilityops-return-processing
|
||||
docker compose restart n8n
|
||||
|
||||
# One-time per environment: imports and activates the scheduled quality-scan workflow.
|
||||
# Same owner-account precondition as n8n-setup above.
|
||||
# Same owner-account and credential preconditions as n8n-setup above (this workflow only
|
||||
# needs "Fleet Ops Service Token").
|
||||
n8n-setup-scan:
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-scheduled-quality-scan.json
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/workflows/fleet-ops-data-quality-scan.json
|
||||
docker compose exec n8n n8n publish:workflow --id=mobilityops-scheduled-quality-scan
|
||||
docker compose restart n8n
|
||||
|
||||
|
||||
+84
-1
@@ -993,7 +993,12 @@ below.
|
||||
via `de0bdea`, followed by evidence commit `f780557` on master. See the "Fleet Ops
|
||||
final localization" entry below for the next (small correction) round on top of this.
|
||||
|
||||
## Fleet Ops final localization: remaining NL/FR gaps, API-error localization, greeting (2026-08-04) — IN PROGRESS on fix/fleet-ops-final-i18n-ux
|
||||
## Fleet Ops final localization: remaining NL/FR gaps, API-error localization, greeting (2026-08-04) — MERGED TO MASTER
|
||||
|
||||
Merged to master via `5f0eaa5`; final evidence commit `c0995b7` added
|
||||
`artifacts/fleet-ops-final-localization/final-summary.md`. Master head at merge:
|
||||
`c0995b762e1cbf37172a08e03645baa6b66aa8d5`. Details below are the in-progress working log
|
||||
kept for reference.
|
||||
|
||||
Branch `fix/fleet-ops-final-i18n-ux`, created from master's post-correction head
|
||||
(`f780557`) — the brief asked for `fix/fleet-ops-final-localization`, but the
|
||||
@@ -1074,3 +1079,81 @@ product scope.
|
||||
`a7ac5ed` (docs), `7851e80` (11D/11F i18n tests), `cda2c32` (clean-checkout
|
||||
evidence), `2e4fb43` (evidence-summary localization fix, found live on Unraid).
|
||||
Deployed commit: `2e4fb43f093bfbdb04c4f74eed1e6c6d9a03c069`.
|
||||
|
||||
## Live n8n + RAGcore integration (2026-08-04) — IN PROGRESS on feat/live-n8n-ragcore-integration
|
||||
|
||||
Branch `feat/live-n8n-ragcore-integration`, from master `c0995b7`. Full brief: treat n8n
|
||||
(`https://n8n.itworx.tech`, existing shared instance) as a third integration layer
|
||||
alongside RAGcore and MCP Hub, owning process orchestration only — Fleet Ops keeps all
|
||||
business rules, authorization, transactions, audit and idempotency. Four canonical
|
||||
workflows required: (1) Vehicle Return Orchestration, (2) Scheduled Data Quality Scan —
|
||||
both pre-existing and now hardened; (3) RAGcore Procedure Sync, (4) Workflow Error
|
||||
Handler — both net-new, not yet built.
|
||||
|
||||
- **Current-state audit**: `docs/live-ai-integration/n8n-current-state.md` documents the
|
||||
live instance (reachable, production webhook base
|
||||
`http://192.168.10.150:5678/webhook/mobilityops-return`), both existing workflows'
|
||||
full node structure, and the findings that drove the security fixes below (webhook
|
||||
Authentication was `None`; both HTTP nodes had `X-Service-Token` hardcoded as a literal
|
||||
header value instead of a credential).
|
||||
- **Security fixes applied and live-validated** (commits `b79d485`, `59cb4c0`): webhook
|
||||
trigger now requires Header Auth (credential `Fleet Ops Webhook Trigger Token`, a new
|
||||
token generated this round — value stored in `.env`/Unraid `.env` only, never
|
||||
printed); the outbound callback HTTP node now uses a `Fleet Ops Service Token` Header
|
||||
Auth credential instead of a literal header value (existing secret copied
|
||||
clipboard-to-clipboard, never typed/echoed). Backend: `X-Fleet-Ops-Trigger-Token`
|
||||
header added to the outbox dispatcher's POST (`backend/app/services/dispatcher.py`),
|
||||
plus a new `MOBILITYOPS_WEBHOOK_TRIGGER_TOKEN` setting/env var. Also hardened
|
||||
`_deliver_one` to treat a 2xx response with a non-JSON-object body as a retryable
|
||||
failure (`malformedResponse`) instead of an unhandled exception — a real failure mode
|
||||
hit live when a workflow errors before its "Respond to Webhook" node runs; regression
|
||||
test `test_deliver_one_treats_empty_2xx_body_as_failure` added. Live-validated: curl
|
||||
probe without the header → `403`; with the header → pass-through; one real end-to-end
|
||||
vehicle return produced one correct execution visible in both n8n and Fleet Ops
|
||||
Audit/Automation. Both workflows explicitly `Publish`ed after the fixes (the editor
|
||||
does not go live on save alone) and both canonical-renamed ("Fleet Ops — Vehicle
|
||||
Return Orchestration", "Fleet Ops — Scheduled Data Quality Scan").
|
||||
- **RAGcore real contract discovered** (not the speculative one the adapter was built
|
||||
against): OpenAPI at `/openapi.json`, health at `/health/live`/`/health/ready` (not
|
||||
`/health`), ingestion via `POST /v1/uploads`, answers via `POST /v1/answers` with
|
||||
`requested_space_ids`, control-plane endpoints require an `Idempotency-Key` header.
|
||||
Bootstrapped a `fleet-ops` application + knowledge space + grant on the real server at
|
||||
`http://192.168.10.150:1237`. **Blocked**: credential issuance for that application
|
||||
failed identically via both the raw API and the admin UI ("authoritative
|
||||
service-account state rejected issuance") — an apparent privilege boundary beyond the
|
||||
interactive admin session. User chose to issue the credential themselves via another
|
||||
mechanism and hand over the token; not yet received. `RAGcoreKnowledgeProvider`
|
||||
(`backend/app/services/knowledge/ragcore.py`) still targets the old speculative
|
||||
endpoints and needs fixing once that token arrives — approved, not started.
|
||||
- **Repository source of truth started** (task in progress): `n8n/workflows/` now holds
|
||||
cleaned definitions for workflows 1-2 — `fleet-ops-vehicle-return.json` (sha256
|
||||
`e13a3087269fc97019a7adf6c6a6a4ee4bd354c2dd7167d4966d4753a48e970e`),
|
||||
`fleet-ops-data-quality-scan.json` (sha256
|
||||
`cc30b28b07dad9f9908a6ea0c564ec4c2f362a3ed71b7e97a7b6894408bb7e2e`) — both credential
|
||||
auth referenced by name only, no secret values. Reconstructed from direct verified
|
||||
inspection of every live node, **not** a literal n8n export/download: the UI's "..."
|
||||
menu has no Download option in this n8n version, and clipboard-based
|
||||
copy/`navigator.clipboard.readText()` extraction timed out twice. Flagged as a known
|
||||
limitation for the final evidence doc. `n8n/workflows/MANIFEST.md` records canonical
|
||||
name/purpose/trigger/contract/credentials/live ID/active-status/checksum for all 4
|
||||
workflows (3-4 marked not-yet-built). `n8n/workflows/check_drift.py` compares a repo
|
||||
definition against the live workflow via n8n's Public API (`X-N8N-API-KEY`, read-only,
|
||||
never auto-overwrites). The old root-level `n8n/mobilityops-return-processing.json`
|
||||
and `n8n/mobilityops-scheduled-quality-scan.json` (pre-integration starters, still
|
||||
carrying the literal-token pattern) are removed; `deploy/unraid/setup-existing-n8n.sh`,
|
||||
`setup-scheduled-scan.sh`, `Makefile` (`n8n-setup`, `n8n-setup-scan`) and
|
||||
`docs/17-runbook.md` updated to import from `n8n/workflows/` and to document the
|
||||
now-required manual credential-creation step (credentials are never scripted or
|
||||
committed).
|
||||
- **Explicitly deferred/forbidden this phase** (per brief): daily AI ops brief, email,
|
||||
Slack, automatic vehicle-status changes, customer communication, billing, general
|
||||
monitoring, autonomous MCP actions. An automatic demo-reset workflow may only be
|
||||
prepared, not activated, once Fleet Ops goes public.
|
||||
- **Exact next action**: build workflow 4 (Workflow Error Handler) — does not depend on
|
||||
the blocked RAGcore token, so it proceeds now. Workflow 3 (RAGcore Procedure Sync) and
|
||||
the `RAGcoreKnowledgeProvider` adapter fix stay blocked until the user supplies a
|
||||
working RAGcore application credential/token. After both new workflows exist: update
|
||||
the Fleet Ops integration status page (task #88), run the full 4-workflow
|
||||
testing/acceptance pass (task #90), and write
|
||||
`artifacts/live-ai-integration/final-summary.md` (task #91, explicitly: no credentials
|
||||
or secret values in that file).
|
||||
|
||||
@@ -3,7 +3,7 @@ set -eu
|
||||
|
||||
container_name="${1:-n8n}"
|
||||
callback_url="${2:-http://192.168.10.150:1236/api/v1/integrations/n8n/return-callback}"
|
||||
source_workflow="${3:-n8n/mobilityops-return-processing.json}"
|
||||
source_workflow="${3:-n8n/workflows/fleet-ops-vehicle-return.json}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Missing deployment .env" >&2
|
||||
@@ -18,12 +18,10 @@ if ! docker inspect "$container_name" >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
callback_token="$(sed -n 's/^MOBILITYOPS_CALLBACK_TOKEN=//p' .env | tail -n 1)"
|
||||
if [ -z "$callback_token" ]; then
|
||||
echo "MOBILITYOPS_CALLBACK_TOKEN is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The workflow file no longer carries the callback token as a literal header value -- both
|
||||
# the webhook trigger and the outbound callback authenticate via named n8n Header Auth
|
||||
# credentials ("Fleet Ops Webhook Trigger Token", "Fleet Ops Service Token"). Those must
|
||||
# exist in the target n8n instance before this workflow is activated; see the echo below.
|
||||
temporary_workflow="$(mktemp /tmp/mobilityops-n8n-workflow.XXXXXX.json)"
|
||||
container_workflow="/tmp/mobilityops-return-processing.json"
|
||||
cleanup() {
|
||||
@@ -32,15 +30,17 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
jq --arg callback_url "$callback_url" --arg callback_token "$callback_token" '
|
||||
(.nodes[] | select(.id == "callback-node") | .parameters.url) = $callback_url |
|
||||
(.nodes[] | select(.id == "callback-node") | .parameters.headerParameters.parameters[] |
|
||||
select(.name == "X-Service-Token") | .value) = $callback_token
|
||||
jq --arg callback_url "$callback_url" '
|
||||
(.nodes[] | select(.id == "callback-node") | .parameters.url) = $callback_url
|
||||
' "$source_workflow" > "$temporary_workflow"
|
||||
|
||||
docker cp "$temporary_workflow" "$container_name:$container_workflow" >/dev/null
|
||||
docker exec "$container_name" n8n import:workflow --input="$container_workflow"
|
||||
docker exec "$container_name" n8n publish:workflow --id=mobilityops-return-processing
|
||||
docker restart "$container_name" >/dev/null
|
||||
|
||||
echo "Published MobilityOps return workflow to existing container ${container_name}"
|
||||
echo "Imported Fleet Ops — Vehicle Return Orchestration into container ${container_name}."
|
||||
echo "Before activating: in the n8n UI, create Header Auth credentials named"
|
||||
echo " 'Fleet Ops Webhook Trigger Token' (value = MOBILITYOPS_WEBHOOK_TRIGGER_TOKEN from .env)"
|
||||
echo " 'Fleet Ops Service Token' (value = MOBILITYOPS_CALLBACK_TOKEN from .env)"
|
||||
echo "then open the workflow and click Publish. This script does not print or transmit"
|
||||
echo "those secret values, and does not restart the container -- restart it yourself once"
|
||||
echo "credentials are wired up and the workflow is published, if required."
|
||||
|
||||
@@ -3,7 +3,7 @@ set -eu
|
||||
|
||||
container_name="${1:-n8n}"
|
||||
scan_url="${2:-http://192.168.10.150:1236/api/v1/integrations/n8n/scheduled-scan}"
|
||||
source_workflow="${3:-n8n/mobilityops-scheduled-quality-scan.json}"
|
||||
source_workflow="${3:-n8n/workflows/fleet-ops-data-quality-scan.json}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Missing deployment .env" >&2
|
||||
@@ -18,12 +18,10 @@ if ! docker inspect "$container_name" >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
callback_token="$(sed -n 's/^MOBILITYOPS_CALLBACK_TOKEN=//p' .env | tail -n 1)"
|
||||
if [ -z "$callback_token" ]; then
|
||||
echo "MOBILITYOPS_CALLBACK_TOKEN is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The workflow file no longer carries the callback token as a literal header value -- the
|
||||
# scan request authenticates via the named n8n Header Auth credential ("Fleet Ops Service
|
||||
# Token"), which must exist in the target n8n instance before this workflow is activated;
|
||||
# see the echo below.
|
||||
temporary_workflow="$(mktemp /tmp/mobilityops-n8n-workflow.XXXXXX.json)"
|
||||
container_workflow="/tmp/mobilityops-scheduled-quality-scan.json"
|
||||
cleanup() {
|
||||
@@ -32,15 +30,16 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
jq --arg scan_url "$scan_url" --arg callback_token "$callback_token" '
|
||||
(.nodes[] | select(.id == "scan-node") | .parameters.url) = $scan_url |
|
||||
(.nodes[] | select(.id == "scan-node") | .parameters.headerParameters.parameters[] |
|
||||
select(.name == "X-Service-Token") | .value) = $callback_token
|
||||
jq --arg scan_url "$scan_url" '
|
||||
(.nodes[] | select(.id == "scan-node") | .parameters.url) = $scan_url
|
||||
' "$source_workflow" > "$temporary_workflow"
|
||||
|
||||
docker cp "$temporary_workflow" "$container_name:$container_workflow" >/dev/null
|
||||
docker exec "$container_name" n8n import:workflow --input="$container_workflow"
|
||||
docker exec "$container_name" n8n publish:workflow --id=mobilityops-scheduled-quality-scan
|
||||
docker restart "$container_name" >/dev/null
|
||||
|
||||
echo "Published MobilityOps scheduled quality-scan workflow to existing container ${container_name}"
|
||||
echo "Imported Fleet Ops — Scheduled Data Quality Scan into container ${container_name}."
|
||||
echo "Before activating: in the n8n UI, create a Header Auth credential named"
|
||||
echo " 'Fleet Ops Service Token' (value = MOBILITYOPS_CALLBACK_TOKEN from .env)"
|
||||
echo "then open the workflow and click Publish. This script does not print or transmit"
|
||||
echo "that secret value, and does not restart the container -- restart it yourself once"
|
||||
echo "the credential is wired up and the workflow is published, if required."
|
||||
|
||||
+15
-15
@@ -16,7 +16,7 @@ Steps:
|
||||
4. return a stable workflow result;
|
||||
5. on errors, fail visibly so the outbox dispatcher can retry.
|
||||
|
||||
The starter export is `n8n/mobilityops-return-processing.json`. Claude may correct its credentials and callback route but must preserve idempotency.
|
||||
The canonical, live-validated definition is `n8n/workflows/fleet-ops-vehicle-return.json` (see `n8n/workflows/MANIFEST.md`); it authenticates via named Header Auth credentials rather than a literal token, per the live-hardening pass documented in `docs/live-ai-integration/n8n-current-state.md`.
|
||||
|
||||
## Second live workflow: scheduled quality scan
|
||||
|
||||
@@ -39,24 +39,24 @@ Steps:
|
||||
open, so a duplicate or overlapping trigger (a manual test run firing close to the
|
||||
scheduled one, or a retried HTTP call) does no duplicate domain work.
|
||||
|
||||
The starter export is `n8n/mobilityops-scheduled-quality-scan.json`, imported and
|
||||
published the same way as the return-processing workflow (see
|
||||
`deploy/unraid/setup-scheduled-scan.sh` and `docs/17-runbook.md`). It ships with
|
||||
`"active": false` so it cannot fire against any environment until deliberately
|
||||
published with a real service token.
|
||||
The canonical, live-validated definition is `n8n/workflows/fleet-ops-data-quality-scan.json`
|
||||
(see `n8n/workflows/MANIFEST.md`), imported and published the same way as the return
|
||||
workflow (see `deploy/unraid/setup-scheduled-scan.sh` and `docs/17-runbook.md`). It is
|
||||
active on the live instance; a fresh import ships inactive until credentials are wired up
|
||||
and it is deliberately published.
|
||||
|
||||
## Deferred: knowledge sync
|
||||
## RAGcore procedure sync (in progress)
|
||||
|
||||
Input: manual trigger or manifest-changed event.
|
||||
RAGcore is now reachable in this environment; a live inspection of its real contract is
|
||||
recorded in `docs/live-ai-integration/n8n-current-state.md`. Workflow 3, "Fleet Ops —
|
||||
RAGcore Procedure Sync", is being built against that real contract (not the sketch
|
||||
originally in this section) — see `n8n/workflows/MANIFEST.md` for current status.
|
||||
|
||||
Steps:
|
||||
## Workflow error handler (in progress)
|
||||
|
||||
1. read the fixed knowledge manifest;
|
||||
2. call RAGcore ingestion/sync API;
|
||||
3. record per-document results through MobilityOps integration status API.
|
||||
|
||||
Deferred until RAGcore's live ingestion API is available in this environment; must not
|
||||
delay or block the core demo.
|
||||
Workflow 4, "Fleet Ops — Workflow Error Handler", is a central technical workflow attached
|
||||
to workflows 1-3 via n8n's per-workflow "Error Workflow" setting, reporting bounded,
|
||||
secret-free failure details to Fleet Ops. See `n8n/workflows/MANIFEST.md` for status.
|
||||
|
||||
## Outbox dispatcher
|
||||
|
||||
|
||||
+11
-2
@@ -48,7 +48,7 @@ longer gates this. This is a one-time step per fresh `docker compose down -v`:
|
||||
which runs:
|
||||
|
||||
```bash
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-return-processing.json
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/workflows/fleet-ops-vehicle-return.json
|
||||
docker compose exec n8n n8n publish:workflow --id=mobilityops-return-processing
|
||||
docker compose restart n8n
|
||||
```
|
||||
@@ -56,6 +56,12 @@ longer gates this. This is a one-time step per fresh `docker compose down -v`:
|
||||
(`n8n import:workflow` always leaves the workflow deactivated regardless of its
|
||||
`"active"` field; `publish:workflow` + a restart is what actually activates it.)
|
||||
|
||||
Before it will actually process a return, create two Header Auth credentials in the n8n
|
||||
UI — `Fleet Ops Webhook Trigger Token` (value: `MOBILITYOPS_WEBHOOK_TRIGGER_TOKEN` from
|
||||
`.env`) and `Fleet Ops Service Token` (value: `MOBILITYOPS_CALLBACK_TOKEN` from `.env`) —
|
||||
the workflow's webhook trigger and outbound HTTP call reference these credentials by
|
||||
name; no secret value is embedded in the workflow file itself.
|
||||
|
||||
Verify the full round trip:
|
||||
|
||||
```bash
|
||||
@@ -77,11 +83,14 @@ make n8n-setup-scan
|
||||
which runs:
|
||||
|
||||
```bash
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-scheduled-quality-scan.json
|
||||
docker compose exec n8n n8n import:workflow --input=//imports/workflows/fleet-ops-data-quality-scan.json
|
||||
docker compose exec n8n n8n publish:workflow --id=mobilityops-scheduled-quality-scan
|
||||
docker compose restart n8n
|
||||
```
|
||||
|
||||
This workflow also needs the `Fleet Ops Service Token` Header Auth credential created in
|
||||
the n8n UI before a run will succeed.
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
|
||||
+15
-9
@@ -1,13 +1,19 @@
|
||||
# n8n workflows
|
||||
|
||||
`mobilityops-return-processing.json` is a starter import for the required live workflow.
|
||||
Canonical, live-validated workflow definitions live under `n8n/workflows/`. Each file is a
|
||||
cleaned export (secret values replaced by named-credential references, never literal
|
||||
tokens) of the workflow actually running on `https://n8n.itworx.tech`. See
|
||||
`n8n/workflows/MANIFEST.md` for the authoritative list: canonical name, purpose, trigger,
|
||||
required credentials, live workflow ID, active status and a checksum of each file.
|
||||
|
||||
Before final validation Claude must:
|
||||
The two pre-integration starter files that used to live directly under `n8n/`
|
||||
(`mobilityops-return-processing.json`, `mobilityops-scheduled-quality-scan.json`) have been
|
||||
retired — they predate the live n8n validation pass and embedded the service token as a
|
||||
literal header value instead of a Header Auth credential. Do not resurrect them; the
|
||||
`n8n/workflows/` versions are the superseding source of truth and are what
|
||||
`deploy/unraid/setup-existing-n8n.sh` / `setup-scheduled-scan.sh` import.
|
||||
|
||||
1. import or provision the workflow;
|
||||
2. configure a service credential for the MobilityOps callback;
|
||||
3. activate the webhook;
|
||||
4. ensure event ID is used as the idempotency key;
|
||||
5. validate online success and offline retry behaviour.
|
||||
|
||||
The callback URL in the starter file is intentionally environment-driven and may require an n8n expression or credential adjustment.
|
||||
Deploying a workflow from these files into a fresh or existing n8n instance still requires a
|
||||
one-time manual step: create the named Header Auth credentials in the n8n UI (see each
|
||||
workflow's `credentials` block and the manifest) before publishing. This is deliberate —
|
||||
credential values are never committed to the repository.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# n8n workflow manifest
|
||||
|
||||
Source of truth for the four canonical Fleet Ops n8n workflows. Definitions in this
|
||||
directory are cleaned exports of the live workflows on `https://n8n.itworx.tech` —
|
||||
credential values are never embedded; nodes reference named n8n credentials instead. Run
|
||||
`n8n/workflows/check_drift.py` to compare a live workflow against its repo definition.
|
||||
|
||||
## 1. Fleet Ops — Vehicle Return Orchestration
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| File | `fleet-ops-vehicle-return.json` |
|
||||
| Purpose | Orchestrate the post-return follow-up (cleaning vs. attention-required) once Fleet Ops emits a `vehicle.returned.v1` outbox event, and report the result back to Fleet Ops. |
|
||||
| Trigger | Production webhook, `POST /webhook/mobilityops-return`, Header Auth (`Fleet Ops Webhook Trigger Token`) |
|
||||
| Event contract | `contracts/events.schema.json`, `event_type: vehicle.returned.v1` (envelope: `event_id`, `event_type`, `occurred_at`, `correlation_id`, `aggregate`, `data`) |
|
||||
| Required credentials | `Fleet Ops Webhook Trigger Token` (Header Auth, on the trigger); `Fleet Ops Service Token` (Header Auth, on the outbound HTTP call) |
|
||||
| Live workflow ID | `mobilityops-return-processing` |
|
||||
| Active status (as of 2026-08-04) | Active / Published |
|
||||
| Checksum (sha256) | `e13a3087269fc97019a7adf6c6a6a4ee4bd354c2dd7167d4966d4753a48e970e` |
|
||||
|
||||
## 2. Fleet Ops — Scheduled Data Quality Scan
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| File | `fleet-ops-data-quality-scan.json` |
|
||||
| Purpose | Periodically (and on-demand) run the Fleet Ops data-quality scan and summarize created-issue counts per rule. |
|
||||
| Trigger | Schedule Trigger (hourly, Europe/Brussels instance timezone) + Manual Trigger for on-demand test runs |
|
||||
| Event contract | N/A — HTTP-triggered scan call, no inbound event envelope. Request: `POST /api/v1/integrations/n8n/scheduled-scan`, Header Auth. |
|
||||
| Required credentials | `Fleet Ops Service Token` (Header Auth, on the scan HTTP call) |
|
||||
| Live workflow ID | `mobilityops-scheduled-quality-scan` |
|
||||
| Active status (as of 2026-08-04) | Active / Published |
|
||||
| Checksum (sha256) | `cc30b28b07dad9f9908a6ea0c564ec4c2f362a3ed71b7e97a7b6894408bb7e2e` |
|
||||
|
||||
## 3. Fleet Ops — RAGcore Procedure Sync
|
||||
|
||||
Not yet built. Blocked on a RAGcore application credential (scope `sources:sync`) for the
|
||||
`fleet-ops` application, to be provided by the project owner. Will sync
|
||||
`n8n/workflows/fleet-ops-ragcore-procedure-sync.json` against the real RAGcore contract
|
||||
(`POST /v1/uploads`, `GET /v1/knowledge-spaces`, etc. — see
|
||||
`contracts/ragcore-contract-assumptions.md` and the live inspection notes in
|
||||
`docs/live-ai-integration/n8n-current-state.md`).
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| File | `fleet-ops-ragcore-procedure-sync.json` (not yet created) |
|
||||
| Live workflow ID | — |
|
||||
| Active status | Not built |
|
||||
|
||||
## 4. Fleet Ops — Workflow Error Handler
|
||||
|
||||
Not yet built. Central error workflow to be attached to workflows 1-3 via n8n's
|
||||
per-workflow "Error Workflow" setting. Reports bounded, secret-free failure details to a
|
||||
new Fleet Ops automation-failure endpoint.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| File | `fleet-ops-error-handler.json` (not yet created) |
|
||||
| Live workflow ID | — |
|
||||
| Active status | Not built |
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report drift between the repo's cleaned workflow definitions and the live n8n instance.
|
||||
|
||||
Read-only: this script only issues GET requests against n8n's Public API. It never writes,
|
||||
imports, activates, or otherwise modifies anything in n8n -- fixing drift is a deliberate,
|
||||
reviewed action a human takes in the n8n UI (or via a separate, explicit import step), not
|
||||
something this script does automatically.
|
||||
|
||||
Usage:
|
||||
N8N_BASE_URL=https://n8n.itworx.tech N8N_API_KEY=... python n8n/workflows/check_drift.py
|
||||
|
||||
N8N_API_KEY must be an n8n Public API key (n8n UI -> Settings -> API), not a session cookie
|
||||
and not a workflow credential. It is read from the environment only and is never printed.
|
||||
|
||||
Exit code is 0 when every checked workflow matches the live instance, 1 when any drift (or
|
||||
a fetch error) is found, so this is safe to wire into CI as a non-blocking check.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
WORKFLOWS_DIR = Path(__file__).parent
|
||||
|
||||
# (repo file name, live workflow ID) -- kept in sync with MANIFEST.md by hand, since the
|
||||
# manifest is the human-readable source of truth and this is just its machine-checkable echo.
|
||||
KNOWN_WORKFLOWS = [
|
||||
("fleet-ops-vehicle-return.json", "mobilityops-return-processing"),
|
||||
("fleet-ops-data-quality-scan.json", "mobilityops-scheduled-quality-scan"),
|
||||
]
|
||||
|
||||
# Fields that legitimately differ between a committed definition and the live instance
|
||||
# (instance-assigned identifiers, timestamps, UI-only cosmetics) and must not be reported
|
||||
# as drift.
|
||||
_VOLATILE_TOP_LEVEL_KEYS = {
|
||||
"versionId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"pinData",
|
||||
"staticData",
|
||||
"shared",
|
||||
"triggerCount",
|
||||
"isArchived",
|
||||
}
|
||||
_VOLATILE_NODE_KEYS = {"position", "webhookId"}
|
||||
|
||||
|
||||
def _strip_credential_ids(value: Any) -> Any:
|
||||
"""Live workflows carry instance-specific credential IDs alongside the credential name
|
||||
(e.g. {"id": "3", "name": "Fleet Ops Service Token"}). The repo definitions intentionally
|
||||
omit the id, since it's meaningless outside the instance that issued it. Drop it from both
|
||||
sides so credential *references* are compared by name only."""
|
||||
if isinstance(value, dict):
|
||||
if set(value.keys()) <= {"id", "name"} and "name" in value:
|
||||
return {"name": value["name"]}
|
||||
return {k: _strip_credential_ids(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_strip_credential_ids(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _normalize_node(node: dict[str, Any]) -> dict[str, Any]:
|
||||
cleaned = {k: v for k, v in node.items() if k not in _VOLATILE_NODE_KEYS}
|
||||
return _strip_credential_ids(cleaned)
|
||||
|
||||
|
||||
def _normalize_workflow(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
nodes_by_name = {n["name"]: _normalize_node(n) for n in doc.get("nodes", [])}
|
||||
return {
|
||||
"name": doc.get("name"),
|
||||
"active": doc.get("active"),
|
||||
"nodes": nodes_by_name,
|
||||
"connections": doc.get("connections", {}),
|
||||
"settings": doc.get("settings", {}),
|
||||
}
|
||||
|
||||
|
||||
def _diff(path: str, local: Any, live: Any, out: list[str]) -> None:
|
||||
if isinstance(local, dict) and isinstance(live, dict):
|
||||
for key in sorted(set(local) | set(live)):
|
||||
if key in _VOLATILE_TOP_LEVEL_KEYS:
|
||||
continue
|
||||
if key not in live:
|
||||
out.append(f"{path}.{key}: present in repo, missing live")
|
||||
elif key not in local:
|
||||
out.append(f"{path}.{key}: present live, missing in repo")
|
||||
else:
|
||||
_diff(f"{path}.{key}", local[key], live[key], out)
|
||||
elif local != live:
|
||||
out.append(f"{path}: repo={local!r} live={live!r}")
|
||||
|
||||
|
||||
def fetch_live_workflow(base_url: str, api_key: str, workflow_id: str) -> dict[str, Any]:
|
||||
url = f"{base_url.rstrip('/')}/api/v1/workflows/{workflow_id}"
|
||||
request = urllib.request.Request(url, headers={"X-N8N-API-KEY": api_key, "Accept": "application/json"})
|
||||
with urllib.request.urlopen(request, timeout=15) as response: # noqa: S310 (fixed https base url from env)
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
base_url = os.environ.get("N8N_BASE_URL")
|
||||
api_key = os.environ.get("N8N_API_KEY")
|
||||
if not base_url or not api_key:
|
||||
print(
|
||||
"Set N8N_BASE_URL and N8N_API_KEY (an n8n Public API key) in the environment.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
any_drift = False
|
||||
for filename, workflow_id in KNOWN_WORKFLOWS:
|
||||
repo_path = WORKFLOWS_DIR / filename
|
||||
local_doc = json.loads(repo_path.read_text(encoding="utf-8"))
|
||||
|
||||
try:
|
||||
live_doc = fetch_live_workflow(base_url, api_key, workflow_id)
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(f"[{filename}] FAILED to fetch live workflow {workflow_id}: HTTP {exc.code}")
|
||||
any_drift = True
|
||||
continue
|
||||
except urllib.error.URLError as exc:
|
||||
print(f"[{filename}] FAILED to fetch live workflow {workflow_id}: {exc.reason}")
|
||||
any_drift = True
|
||||
continue
|
||||
|
||||
differences: list[str] = []
|
||||
_diff(filename, _normalize_workflow(local_doc), _normalize_workflow(live_doc), differences)
|
||||
|
||||
if differences:
|
||||
any_drift = True
|
||||
print(f"[{filename}] DRIFT from live workflow {workflow_id}:")
|
||||
for line in differences:
|
||||
print(f" - {line}")
|
||||
else:
|
||||
print(f"[{filename}] matches live workflow {workflow_id}")
|
||||
|
||||
return 1 if any_drift else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+18
-53
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mobilityops-scheduled-quality-scan",
|
||||
"name": "MobilityOps - Scheduled Quality Scan",
|
||||
"name": "Fleet Ops — Scheduled Data Quality Scan",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
@@ -17,10 +17,7 @@
|
||||
"name": "Hourly schedule",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [
|
||||
240,
|
||||
220
|
||||
]
|
||||
"position": [240, 220]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
@@ -28,23 +25,17 @@
|
||||
"name": "Manual test trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
240,
|
||||
400
|
||||
]
|
||||
"position": [240, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{$env.MOBILITYOPS_API_URL || 'http://api:8000'}}/api/v1/integrations/n8n/scheduled-scan",
|
||||
"url": "http://192.168.10.150:1236/api/v1/integrations/n8n/scheduled-scan",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "X-Service-Token",
|
||||
"value": "={{$env.MOBILITYOPS_CALLBACK_TOKEN}}"
|
||||
}
|
||||
]
|
||||
"parameters": []
|
||||
},
|
||||
"options": {
|
||||
"timeout": 15000
|
||||
@@ -54,10 +45,12 @@
|
||||
"name": "Run quality scan",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
520,
|
||||
300
|
||||
]
|
||||
"position": [520, 300],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"name": "Fleet Ops Service Token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -67,52 +60,24 @@
|
||||
"name": "Summarize result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
780,
|
||||
300
|
||||
]
|
||||
"position": [780, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Hourly schedule": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Run quality scan",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
"main": [[{ "node": "Run quality scan", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Manual test trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Run quality scan",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
"main": [[{ "node": "Run quality scan", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Run quality scan": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
"main": [[{ "node": "Summarize result", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"active": false,
|
||||
"versionId": "22222222-2222-4222-8222-222222222222",
|
||||
"active": true,
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false
|
||||
},
|
||||
+24
-52
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"id": "mobilityops-return-processing",
|
||||
"name": "MobilityOps - Vehicle Return Processing",
|
||||
"name": "Fleet Ops — Vehicle Return Orchestration",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "mobilityops-return",
|
||||
"authentication": "headerAuth",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
@@ -13,11 +14,13 @@
|
||||
"name": "Return webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
240,
|
||||
300
|
||||
],
|
||||
"webhookId": "mobilityops-return"
|
||||
"position": [240, 300],
|
||||
"webhookId": "mobilityops-return",
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"name": "Fleet Ops Webhook Trigger Token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -27,25 +30,20 @@
|
||||
"name": "Validate and derive follow-up",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
500,
|
||||
300
|
||||
]
|
||||
"position": [500, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{$env.MOBILITYOPS_CALLBACK_URL || 'http://api:8000/api/v1/integrations/n8n/return-callback'}}",
|
||||
"url": "http://192.168.10.150:1236/api/v1/integrations/n8n/return-callback",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Idempotency-Key",
|
||||
"value": "={{$json.event_id}}"
|
||||
},
|
||||
{
|
||||
"name": "X-Service-Token",
|
||||
"value": "={{$env.MOBILITYOPS_CALLBACK_TOKEN}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -59,10 +57,12 @@
|
||||
"name": "Record follow-up",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
760,
|
||||
300
|
||||
]
|
||||
"position": [760, 300],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"name": "Fleet Ops Service Token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -74,54 +74,26 @@
|
||||
"name": "Return result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.4,
|
||||
"position": [
|
||||
1020,
|
||||
300
|
||||
]
|
||||
"position": [1020, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Return webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Validate and derive follow-up",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
"main": [[{ "node": "Validate and derive follow-up", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Validate and derive follow-up": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Record follow-up",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
"main": [[{ "node": "Record follow-up", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Record follow-up": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Return result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
"main": [[{ "node": "Return result", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"active": true,
|
||||
"versionId": "11111111-1111-4111-8111-111111111111",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user