Skip to content
← All notes

Workspace 42 — Technical Companion

April 11, 2026 · 6 min read

This is the deep-dive companion to Workspace 42 — How I Stopped Losing Context Between My Own Days. Start there if you haven't. This post covers the workflow architecture and the decisions behind it.


Infrastructure

Workspace 42 runs on a small Linux VPS in my home region, locked to a single timezone for deterministic cron behaviour. Two containers on a private Docker network: n8n (SQLite backend, bound only to the loopback interface — never publicly reachable) and Traefik as the edge router with Let's Encrypt TLS and the usual security headers injected at the middleware layer.

That's the whole stack. No Postgres, no Redis, no job queue. n8n's SQLite file doubles as the workflow database, the execution history, and the credential store.

Scripts over nodes

The most important design choice in the stack. Every active workflow is just two nodes: a scheduleTrigger and an executeCommand that shells out to a plain .mjs script. The n8n canvas is a scheduler and a shell. All business logic lives in files.

Why:

  • Testable. The scripts are plain Node.js — runnable locally, mockable, unit-testable. Validating behaviour doesn't require clicking through a visual canvas.
  • Greppable and diffable. When something breaks at 2 AM, I want grep over a folder, not clicks through seventeen node configuration panels. Git diffs on a .mjs file are readable; diffs on an n8n workflow JSON export are not.
  • Portable. If I ever move off n8n, the scripts work anywhere Node.js does.

The tradeoff: the scripts read n8n's internal SQLite and decrypt credentials directly, which couples them to n8n's storage format. A major n8n upgrade could break that layer. I'm carrying that risk consciously because the upside — real code, real files, real version control — is worth it.

One workflow (the AI Inbox → TickTick promotion) still runs as a full node chain. It's on the list to migrate.

Credentials

Four credentials live in n8n (no secrets reproduced here, obviously): a Notion API token used by every active workflow, a header-auth variant for direct HTTP calls, an OpenAI key for AI Inbox proposal generation, and a TickTick OAuth2 entry that's staged but not active — promotion currently writes to a Notion-side proxy that syncs externally.

Notion databases

Five databases, referenced by name in the scripts rather than UUID, all living at workspace root:

  • Daily Notes — daily journaling with Date, Status, Summary, Notes.
  • MeetingsMeeting Date + a relation back to the matching Daily Note.
  • AI Inbox — LLM-generated proposals, gated by Status ∈ {Proposed, Promoted, Discarded}.
  • Work:TickTick — promoted tasks destined for the TickTick external sync.
  • Ops Log — script execution logging.

A central dashboard page gets its "today" reference link patched by the daily creation script. All databases live at workspace root (not nested under the dashboard) so the API can manage them without parent-scope constraints.

Workflow inventory

Five active workflows, all on cron. No webhooks.

ScheduleWorkflowDoes
00:05 dailyCreate today's Daily NoteQueries for today's page; creates it if missing with a formatted title and open status. Idempotent by Date.
00:30 dailyAI Inbox — evaluate open notesReads open Daily Notes + their linked meetings, hashes the content, skips if unchanged, otherwise calls gpt-4o to emit up to 10 structured proposals per note. Closes the note afterward.
01:00 dailyClose old notesSweeps Daily Notes older than 7 days that are still Open and flips them to Closed. Runs after AI Inbox so nothing is archived before evaluation.
*/5Link meetings to Daily NotesFinds meetings whose Daily Note relation is empty, looks up the matching day (or creates one), and sets the relation. Self-healing via poll retries.
*/5Promote AI Inbox → TickTickReads items flagged for promotion, normalizes title and priority, dedupes against the proxy DB, creates the entry, marks the inbox row Promoted, and archives stale discards.

The AI Inbox workflow is the most involved. The prompt returns proposals as structured objects with category (follow-up, action-item, reminder, idea), priority, a verb-first title, a reasoning trail, and free-form notes. A [hash:XXXXXXXX] tag embedded in each proposal prevents regeneration when a note stays open across multiple nights.

Cron schedule

All times in my local timezone:

00:05  —  Create today's Daily Note if missing
00:30  —  AI Inbox: evaluate open notes, generate proposals
01:00  —  Close Daily Notes older than 7 days
*/5    —  Link unlinked meetings to Daily Notes
*/5    —  Promote AI Inbox items to TickTick + cleanup

Ordering is intentional. Daily Note is created first (00:05). AI Inbox runs next (00:30) so it can process notes while they're still open. Closing runs last (01:00) so nothing is archived before being evaluated.

Architecture

Rendering diagram…

Known limitations

  1. Scripts are coupled to n8n's storage format. Decrypting credentials by reading SQLite directly is tightly coupled to the current schema. A Postgres migration or credential format change would break the layer.
  2. Two export flavors can drift. The thin shims in workflows/ are the source of truth; the post-activation exports in live-export/ are artifacts that need regeneration after each deploy.
  3. SQLite on a Docker volume. All state lives in a single file; backup is volume-snapshot-only, and you can't just cp a WAL-mode database without quiescing first.
  4. No replay on missed crons. A host reboot during a cron window just drops that run. At this scale and with idempotent scripts, it hasn't mattered — but it's worth naming.
  5. Notion API doesn't support template-based page creation. Template changes made in Notion's UI have to be mirrored in the script payload manually. The raw API simply doesn't accept a template_id.
  6. The TickTick credential is staged, not active. Promotion writes to a Notion-side proxy that syncs externally; n8n never calls TickTick directly.

This spec is version-controlled alongside the scripts. If something breaks, start here.