Files
compass_teams_nhs/docs/phase-7a-n8n-schedule.md
Sam Rolfe 75e4ff12bf Phase 7a: stable workflow id for the n8n import (AUD-115)
n8n 2.18.5 rejects an import whose workflow JSON has no `id`
(SQLITE_CONSTRAINT: NOT NULL constraint failed: workflow_entity.id). Give the
workflow a fixed id and record that the import -> publish -> active flow was
rehearsed in a throwaway n8n container before asking for approval.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-15 19:53:43 +11:00

5.2 KiB

Phase 7a — the 6:00am n8n schedule (AUD-115)

Phase 7 (AUD-111) proved the delivery path: POST /notify/morning, the per-boy and combined briefs, the Apprise channels, and eight real deliveries. This phase makes the schedule real: one n8n workflow that runs every day at 06:00 and sends the briefs.

What changed

deploy/n8n-morning-brief.json is now a two-node workflow:

Schedule Trigger  "0 6 * * *"  (Australia/Melbourne)
        |
        v
Execute Command   ssh … sam@192.168.20.13 'scripts/morning-brief.sh'

The earlier draft had three nodes (scan via ssh remote → POST /notify/morning). Two problems made that shape wrong here:

  1. ssh remote does not work. The n8n container mounts ~/.ssh read-only, and ~/.ssh/config is a Nix symlink into /nix/store, which does not exist inside the container. The hostname remote does not resolve, and the container key is not in ~/.ssh/authorized_keys (the host alias fails too).
  2. A split scan/send node hides scan failures. If the scan node fails, n8n stops before the HTTP node, so POST /notify/morning never runs and the family gets no brief at all — the opposite of FR-12.

scripts/morning-brief.sh is already the bounded command that does both steps and exits non-zero when the scan or a send fails. One Execute Command node is therefore both simpler and more correct: a failed scan turns the n8n execution red and still sends a brief that opens with the failure (SCAN FAILED), never "nothing due".

Because the single command runs on the host, n8n needs no SCHOOL_NOTIFY_TOKEN and no HTTP node. The token stays in environment.d/school-service.env only.

The SSH trigger key

The n8n container mounts /home/sam/.ssh read-only at /home/node/.ssh. The trigger uses a dedicated key with a forced command, so it can run exactly one thing:

restrict,command="cd /home/sam/paseo/projects/compass_teams_nhs && ./scripts/morning-brief.sh" ssh-ed25519 AAAA…
  • restrict disables pty, agent forwarding, port forwarding, X11, and shell access.
  • command= makes the server ignore whatever command the client sends.
  • The private key lives at /home/sam/.ssh/id_ed25519_school_scan (mode 0600) and is readable only by sam and the n8n container (which already mounts ~/.ssh).
  • The key is not sam's personal key and is not used anywhere else.

Create it with:

deploy/setup-n8n-scan-key.sh            # dry run: print the plan
deploy/setup-n8n-scan-key.sh --apply    # create the key + the authorized_keys line

Import and activate

deploy/import-n8n-morning-brief.sh            # dry run: print the plan
deploy/import-n8n-morning-brief.sh --apply    # import, publish, restart n8n

The script copies the JSON into the container with docker exec -i … sh -c 'cat > …' (docker cp fails against the read-only ~/.ssh mount), runs n8n import:workflow, then n8n publish:workflow --id=<id> (n8n 2.x replaced update:workflow --active), restarts n8n once so the 06:00 schedule registers, and prints n8n list:workflow --active=true.

The JSON carries a stable id (schoolMorningBrf). n8n 2.18.5 refuses an import whose workflow has no id (SQLITE_CONSTRAINT: NOT NULL constraint failed: workflow_entity.id), so the id is required, not cosmetic. Re-importing the same file updates the same workflow instead of adding a second copy.

n8n publish:workflow prints "Changes will not take effect if n8n is running", which is why the import script restarts n8n once. The full import → publish → active check was rehearsed in a throwaway n8n container with a temporary data directory before this plan was proposed; no shared instance was touched.

Verification

docker exec n8n n8n list:workflow --active=true      # the workflow is published
docker exec compass-teams-nhs /app/school notify status

A healthy morning looks like this in notify status:

1 Harry   Harry   email    ok  <date> 06:0x
2 Harry   Harry   whatsapp ok  <date> 06:0x
3 Finn    Finn    email    ok  <date> 06:0x
4 Finn    Finn    whatsapp ok  <date> 06:0x
5 combined Jo     email    ok  <date> 06:0x
6 combined Jo     whatsapp ok  <date> 06:0x
7 combined Sam    email    ok  <date> 06:0x
8 combined Sam    whatsapp ok  <date> 06:0x

Failure drill (never leave an empty day): point the key's forced command at a command that exits non-zero, trigger the workflow, and confirm the n8n execution is red and the brief body starts with the scan failure. Restore the command afterwards.

Proving a run

The Schedule Trigger only fires at 06:00, so the schedule itself is proved by n8n list:workflow --active=true plus the next morning's notify status rows. To prove the full send path without waiting, run the published workflow once by hand:

docker exec n8n n8n execute --id=<workflow-id>

This sends the real briefs, so run it only when a real delivery is wanted. n8n list:workflow prints the id. Both checks are recorded in this issue's final comment.

Not touched

  • Compass / Teams: read-only. Nothing in this phase writes to the school.
  • Other n8n workflows: untouched.
  • Secrets: no address, phone number, Apprise key, session secret, or notify token is written into the workflow JSON, this document, or git.