Files
compass_teams_nhs/docs/phase-7a-n8n-schedule.md
Sam Rolfe e05133281e Phase 7a: first automatic 06:00 run verified green (AUD-115)
n8n execution 1700: mode=trigger, status=success, started 2026-10-16 19:00Z
(06:00 AEDT 2026-10-17). 8/8 rows 27-34 delivered, scans ok for both children.
make verify-n8n exits 0. Documents the acceptance evidence.
2026-10-17 06:31:28 +11:00

11 KiB
Raw Blame History

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 the workflow is

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

Schedule Trigger  "0 6 * * *"  (Australia/Melbourne) ─┐
                                                       ├─▶  SSH node  ──▶  Code check
Manual Trigger  "Run now (manual)"  ───────────────────┘    (restricted key)
  • SSH node — authenticates to .13 as sam with the restricted trigger key and runs the brief on the host. The scan needs node + patchright on .13, so it cannot run inside the n8n container.
  • Code node — the SSH node returns the remote exit code but does not fail on it. The Code node throws when code !== 0, so a failed scan or send turns the n8n execution red (FR-12).
  • Manual Trigger — optional. It lets Sam run the brief by hand from the n8n UI, or with docker exec n8n n8n execute --id=schoolMorningBrf. The schedule fires on its own; the manual trigger only does something when pressed.

scripts/morning-brief.sh is the bounded command. It scans both children, builds the per-boy and combined briefs, sends them through Apprise, and exits non-zero when the scan or a send fails. The brief always opens with the failure when the scan failed — never "nothing due".

The run needs no SCHOOL_NOTIFY_TOKEN in n8n. The token stays in environment.d/school-service.env, which the host script reads.

Why not an Execute Command node

The first design used one n8n-nodes-base.executeCommand node. It cannot work on this instance. n8n 2.x disables that node by default for security (nodes.exclude = ['n8n-nodes-base.executeCommand', 'n8n-nodes-base.localFileTrigger']). An imported workflow with it is marked active in the database but never activates:

Activation of workflow "Compass / NHS — 6:00am morning brief" (schoolMorningBrf) did fail
  with error: "Unrecognized node type: n8n-nodes-base.executeCommand"

The same error affects the pre-existing Tree Info workflow. The SSH node is enabled and, with the restricted key, runs exactly one command — so it is both workable and least-privilege. Enabling Execute Command instead would need NODES_EXCLUDE=[] in the shared n8n service, which re-enables arbitrary shell execution for every workflow.

The SSH trigger key and credential

The n8n container mounts /home/sam/.ssh 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 the key 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

The SSH node needs the private key in an n8n credential (sshPrivateKey, id schoolScanKey). deploy/import-n8n-morning-brief.sh builds that credential from the key file into a mode-0600 temp file, imports it, and removes it. The key is never written into this repository.

Import and activate

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

The script imports the credential, copies the JSON into the container with docker exec -i … sh -c 'cat > …' (docker cp fails against the ~/.ssh mount), runs n8n import:workflow, then n8n publish:workflow --id=schoolMorningBrf (n8n 2.x replaced update:workflow --active), restarts n8n once so the 06:00 schedule registers, and confirms the n8n log shows the workflow activated — the database's active flag alone is not proof.

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 → activate → run and the failure path were rehearsed in a throwaway n8n container with a temporary data directory, using the same n8n 2.18.5 image, before the change was proposed. The shared instance was not touched for the rehearsal.

Verification

docker logs n8n | grep 'Activated workflow "Compass / NHS'
docker exec n8n n8n list:workflow --active=true      # schoolMorningBrf is listed
docker exec compass-teams-nhs /app/school notify status

Rehearsal evidence (throwaway container, same image):

  • activation: Activated workflow "school ssh rehearsal" — no Unrecognized node type
  • success path (forced command echo): status: success
  • failure path (forced command exit 7): status: error, CLI exit 1, message morning-brief.sh failed with exit 7

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

Proving a run

The Schedule Trigger only fires at 06:00, so the schedule is proved by the activation log line 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 -e N8N_RUNNERS_BROKER_PORT=5681 n8n n8n execute --id=schoolMorningBrf

The N8N_RUNNERS_BROKER_PORT override gives this short-lived CLI process its own task broker port. Without it n8n execute stops with "n8n Task Broker's port 5679 is already in use", because the running n8n instance owns 5679. It changes only the one docker exec process; the shared service keeps 5679.

This sends the real briefs, so run it only when a real delivery is wanted.

Verify the scheduled run (read-only)

make verify-n8n          # or: deploy/verify-n8n-scheduled-run.sh

deploy/verify-n8n-scheduled-run.sh never runs the workflow and never sends anything. It checks that the workflow is active, reads the newest scheduled (mode=trigger) execution from the n8n database, and prints the notify rows. A hand run (mode=cli) does not satisfy it. The exit code distinguishes the states:

  • 0 — a scheduled run exists and its newest one finished success.
  • 1 — the workflow is inactive, or the schedule has not fired yet, or the newest scheduled run failed.

It reads the n8n database directly (N8N_DATA_DIR, default /home/sam/Docker/Containers/n8n_data) because n8n 2.x stores scheduled executions with mode=trigger, which n8n list:workflow does not report.

Applied — 2026-10-16

Sam accepted plan revision 2 (257b4b0a), and the change was applied to the shared n8n on .13. Evidence from that heartbeat:

  • deploy/import-n8n-morning-brief.sh --apply: imported 1 credential and 1 workflow, published schoolMorningBrf, restarted n8n once, and the log then showed Activated workflow "Compass / NHS — 6:00am morning brief" (ID: schoolMorningBrf) with no Unrecognized node type error. n8n list:workflow --active=true lists it.
  • A hand run through the published workflow finished "status": "success", "finished": true, CLI exit 0. The SSH node reached sam@192.168.20.13 with the restricted key, ran scripts/morning-brief.sh, scanned both children (Harry 71 items, Finn 43 items, both ok) and delivered 8/8 through Apprise (email + WhatsApp for Harry, Finn, and the combined brief to Jo and Sam).
  • docker exec compass-teams-nhs /app/school notify status: total=26 delivered=26 failed=0, new rows 19–26 (Harry 19–20, Finn 21–22, combined Jo 23–24, Sam 25–26), all ok with sent_at set.

First automatic run — green (2026-10-17)

The Schedule Trigger fired on its own at 06:00 Australia/Melbourne on 2026-10-17 (2026-10-16T19:00Z) and the run was green. This closes the acceptance for AUD-115.

  • n8n execution 1700: mode=trigger, status=success, started 2026-10-16 19:00:00 UTC (06:00 AEDT). The mode=trigger is the point: it is the scheduler, not a hand run.
  • make verify-n8n (deploy/verify-n8n-scheduled-run.sh) exited 0: the workflow is active, the newest scheduled run is green, rows above.
  • Scan at 06:00: Harry ok, Finn ok.
  • Delivery rows 27–34 — 8/8, all ok with sent_at set: Finn email 27 / WhatsApp 28, Harry email 29 / WhatsApp 30, combined Jo email 31 / WhatsApp 32, combined Sam email 33 / WhatsApp 34.
  • docker exec compass-teams-nhs /app/school notify status: total=34 delivered=34 failed=0. The last attempt is combined Sam/whatsapp ok=true at 2026-10-17 06:01.
  • POST /notify/morning?dry_run=1 → 200.

The failure path (a non-zero exit makes the execution red and opens the brief with SCAN FAILED) was proven in the throwaway rehearsal; the live 06:00 run was green, so it was not exercised. The Code node is unchanged. The workflow timezone is honoured: the n8n container runs TZ=Australia/Melbourne, the workflow sets settings.timezone=Australia/Melbourne, and the pre-existing AI Calendar - Family Notifier schedule stores its local 05:00/16:00/20:00 firings as 18:00/05:00/09:00 UTC.

Earlier, an issue monitor fired early (a manual monitor/check-now clears the monitor as it dispatches); once re-armed for 06:30 AEDT it woke the assignee on schedule, and that run recorded the evidence above.

Not touched

  • Compass / Teams: read-only. Nothing in this phase writes to the school.
  • Other n8n workflows: untouched.
  • n8n's global node configuration: untouched. Execute Command stays disabled.
  • Secrets: no address, phone number, Apprise key, session secret, or notify token is written into the workflow JSON, this document, or git. The SSH private key lives in an n8n credential and the host authorized_keys, never in the repository.