Dictate a project name (or short pitch) to Siri, get it dropped in the Notion 📁 Projects DB with Status=Active, then enriched async by an LLM that fills in Area / Status / Link / Target Date / Notes / Icon (page emoji) / and (if the dictation was rich enough) a 1–3 sentence scope blurb in the page body. Siri gets a fast 200 back; enrichment runs after.

Sister workflow to Todo Capture — same architecture, same credentials, same conventions.

Quick Reference

ItemValue
n8n workflow IDe0BX5mVEAqTOynqp
Workflow nameProject Capture (Siri → Notion + LLM enrichment)
Webhook URLhttps://n8n.cloud.mdbook.one/webhook/project-capture
MethodPOST
LLM backendLibreChat agent (Qwen3.5:9b) via lmChatOpenAi
LibreChat agentagent_sxbUMb6A7G1mt5663597u
LLM prompt (git)mikayla/scripts:prompts/project-capture.md
Notion target📁 Projects DB → Status=Active
Statusn8n workflow ACTIVE; iOS Shortcut not yet versioned

Architecture

Split-response pattern — Siri doesn’t wait for the LLM.

Webhook (Siri POST)
  ↓
Create Project           (Notion: name only, Status=Active)
  ↓
Respond to Siri          (200 immediately)
  ↓
Build Enrichment Prompt  (today + raw title; no project list, unlike Todo Capture)
  ↓
Enrich w/ LLM            (chainLlm + lmChatOpenAi → LibreChat)
  ↓
Parse + Build Update     (reads $json.text, validates, builds Notion patch + optional body block)
  ↓
Update Project w/        (Notion PATCH /v1/pages/{pageId} —
  Enrichment              cleaned title, Area, Status, Link, Target Date, Notes,
                          Needs Review, AND page-level icon emoji)
  ↓
If Has Description       (true → append; false → done)
  ↓
Append Description Block (Notion PATCH /v1/blocks/{pageId}/children —
                          paragraph block with the LLM scope blurb)

iOS Shortcut Spec

POST to the webhook with:

  • Header: Authorization: <bearer-or-shared-secret> — same shared secret as Todo Capture (reuses the Header Auth account cred lgNSPgd9yTwylM8b)
  • Header: Content-Type: application/json
  • Body: { "title": "<dictated text>" }

Raw dictation goes in as-is — the LLM splits it into clean title + link + notes + icon + (optional) description. Short dictations like “new project rewire the rack” still work; longer ones like “build a mobile-first dashboard for my Bambu Lab printers to replace Bambu Handy via reverse proxy” get a paragraph appended to the page body. Either way, the page gets an emoji icon.

LLM Enrichment

Uses the same LangChain pattern as Todo Capture:

  • @n8n/n8n-nodes-langchain.chainLlm — main reasoning node
  • @n8n/n8n-nodes-langchain.lmChatOpenAi attached via ai_languageModel — model node
  • openAiApi credential points at LibreChat (chat.mdbook.me); the model node targets agent agent_sxbUMb6A7G1mt5663597u
  • Output is read as $json.text in the Parse node

The full classifier system prompt lives in git at mikayla/scripts:prompts/project-capture.md. Source of truth for the running prompt is still the LibreChat agent UI — keep them in sync by hand for now.

Classifier output schema

{
  "title": "cleaned project name",
  "areas": ["Homelab", "Code", ...],
  "status": "Active" | "Backlog" | "Paused",
  "link": "extracted URL or null",
  "target_date": "YYYY-MM-DD or null",
  "notes": "extra context or null",
  "description": "1–3 sentence body blurb, or null for one-liner dictations",
  "icon": "single emoji, required"
}

Rules baked into the prompt

  • Areas: Homelab / Code / Coaching / Teaching / Audio / Personal / Health / Errands / Music (multi-select; 1–2 commonly apply)
  • Status: Active (default), Backlog (someday/idea), Paused (explicitly on hold). Done/Archived are never LLM-settable for a freshly-created project.
  • Target dates ISO YYYY-MM-DD only, from explicit refs (incl. quarter-ends, “in N months”); vague refs → null
  • description is conditional: only emitted when the dictation contains substantive scope/goal info beyond the name. One-liners → null.
  • icon is always required (single emoji). LLM prefers a subject-specific emoji over a generic area emoji; falls back to a per-area emoji when no specific match fits. Set as the Notion page-level icon, not a property.
  • Today’s date injected by n8n in America/New_York; Parse node validates ISO before sending to Notion

Credentials Needed

All reused from Todo Capture:

  1. Notion internal integration token (LmNPdmds7KJiP78a) — must have access to the 📁 Projects DB
  2. HTTP Header Auth credential (lgNSPgd9yTwylM8b) — for the inbound webhook Authorization check
  3. OpenAI API credential pointing at LibreChat (E3JKwhS3rOMyn72x, shared “LibreChat- Youtube Summary” cred) — provides the LibreChat connection; the model node selects the dedicated Project Capture agent

No new creds required.

Schema Additions (Projects DB)

Already applied to the 📁 Projects DB (759dacc380be4c6594ab95d7d002d605):

  • Notes — rich_text — extra context the LLM extracted but couldn’t slot into a structured field
  • Needs Review — checkbox — set true when LLM JSON fails to parse/validate, or when nothing useful comes back
  • (recommended, not yet created) view ⚠️ Needs Review filtered to Needs Review = true for triage

Existing Projects DB properties used: Name (title), Area (multi_select), Status (select), Link (url), Target Date (date). The page icon is set on the page object itself — no schema property needed.

Gotchas

  • Database ID vs Data source ID. Notion’s public REST API wants the URL-style database ID 759dacc380be4c6594ab95d7d002d605. The collection://811b3380-... data source ID is for internal MCP/native nodes only. n8n HTTP nodes hit the public API, so use the database ID.
  • URL property key. This DB’s URL field is named Link, not URL, so the JSON key is literally Link — no userDefined: prefix needed (only the Tasks DB’s URL field has that quirk).
  • Page icon lives on the page, not in properties. The PATCH body sets icon at the top level alongside properties: {"icon": {"type": "emoji", "emoji": "🖨️"}, "properties": {...}}. Don’t try to model it as a property.
  • LangChain output shape. chainLlm returns the model text on $json.text, NOT $json.output or $json.message.content.
  • Page body is a separate API call. /v1/pages/{id} PATCH doesn’t accept children. Body blocks have to be appended via /v1/blocks/{pageId}/children PATCH. The If Has Description node gates this so one-liner dictations don’t fire an empty append.
  • Schema is load-bearing. If you change field names, types, or value enums in the classifier prompt, also update the Parse + Build Update node in n8n. Validation failure → Needs Review=true and the page keeps its raw title.
  • Default status is Active in n8n, not in the LLM. The initial Create Project node forces Status=Active so the page lands in your active board immediately. The LLM can later demote it to Backlog/Paused via the enrichment patch.
  • Failure mode. Malformed JSON or all-empty enrichment → Needs Review=true, no body block appended, no icon set. Manual triage via the Needs Review view.
  • Timezone. All date math runs in America/New_York, injected from n8n’s current time, not the LLM’s notion of “today.”

Disabling

n8n UI → workflow e0BX5mVEAqTOynqp → toggle Active off. Or delete the webhook credential to break it at the auth layer without touching the workflow itself.

Changelog

  • 2026-06-03 — First doc refresh since creation. Confirmed the n8n workflow is active (Quick Reference previously said INACTIVE). Fixed the sister-workflow link to the Quartz path. Clarified that the shared LibreChat cred provides the connection while the model node selects the dedicated agent. Open item: confirm agent agent_sxbUMb6A7G1mt5663597u is actually provisioned in LibreChat — the Enrich w/ LLM node’s own note still flags the agent ID as a placeholder.