plugins/lisa/skills/notion-write-prd/SKILL.md
Creates or idempotently updates a PRD as a page in the configured Notion PRD database, setting the lifecycle Status property to the draft value by default (or the ready value when initial_role is ready so lisa:notion-prd-intake auto-claims it). The Notion PRD-source writer behind lisa:prd-source-write. Dedupes by a stable marker embedded in the page (matched by marker, never by title). All Notion access goes through lisa:notion-access — never call the Notion API or MCP directly.
npx skillsauth add codyswanngt/lisa notion-write-prdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Create (or update) a PRD page in the configured Notion PRD database. Invoked by
lisa:prd-source-write when source = notion; do not call directly from a vendor-neutral caller.
All Notion operations go through lisa:notion-access (the access chokepoint) — never curl the
Notion API or call a mcp__*notion* tool yourself.
$ARGUMENTS carries the lisa:prd-source-write spec: title, body (full PRD markdown),
initial_role (draft | ready, default draft), dedupe_key, marker, optional source_ref.
read_g() { local lv gv; lv=$(jq -r "$1 // empty" .lisa.config.local.json 2>/dev/null); gv=$(jq -r "$1 // empty" .lisa.config.json 2>/dev/null); echo "${lv:-${gv:-$2}}"; }
PRD_DB=$(read_g '.notion.prdDatabaseId' '')
[ -z "$PRD_DB" ] && { echo "Error: notion.prdDatabaseId not set in .lisa.config.json."; exit 1; }
STATUS_PROP=$(read_g '.notion.statusProperty' 'Status')
# Resolve the FULL PRD Status vocabulary from config (never hard-code) so the past-ready check is
# correct even when a project renamed any Status value.
DRAFT=$(read_g '.notion.values.draft' 'Draft')
READY=$(read_g '.notion.values.ready' 'Ready')
IN_REVIEW=$(read_g '.notion.values.in_review' 'In Review')
BLOCKED=$(read_g '.notion.values.blocked' 'Blocked')
TICKETED=$(read_g '.notion.values.ticketed' 'Ticketed')
SHIPPED=$(read_g '.notion.values.shipped' 'Shipped')
VERIFIED=$(read_g '.notion.values.verified' 'Verified')
# "Progressed past ready" set (never down-rank): the resolved in_review/blocked/ticketed/shipped/verified.
PROGRESSED=("$IN_REVIEW" "$BLOCKED" "$TICKETED" "$SHIPPED" "$VERIFIED")
Resolve the target Status value from initial_role: ready → $READY, otherwise $DRAFT.
The marker is embedded in the page (as the first body block). Find an existing PRD page in the DB
carrying it — match the marker, never the title:
lisa:notion-access operation: search query: "<marker>" (Notion indexes page content).$PRD_DB. If source_ref was passed, target that page
directly and skip the search.Markdown → Notion blocks (conversion boundary). Convert the PRD markdown to Notion block objects:
#/##/### → heading_1/2/3, paragraphs → paragraph, -/* → bulleted_list_item, 1. →
numbered_list_item, fenced code → code. The Notion API caps a single request at 100 blocks
and ~2000 characters of rich text per block: split long paragraphs across blocks, and if the PRD
exceeds 100 blocks, create the page with the first ≤100 blocks then add the remainder with batched
operation: append-blocks calls (≤100 each). When the MCP substrate is active, create-page may
accept the markdown content directly (it performs this conversion) — prefer that; the explicit block
conversion is the curl-substrate path.
Marker + usage-ledger preservation (both paths). The page must always carry exactly one
marker. On CREATE the marker is the first body block; on UPDATE never remove it. Never write a markerless body. Never write a markerless page. If the existing page content already contains the canonical managed ## Lisa Usage section, preserve that section when regenerating the page body unless the caller intentionally supplied an updated canonical section; use the shared usage-accounting serializer/merge path rather than freehand block edits to ledger rows.
CREATE:
<!-- $MARKER -->), then the converted PRD blocks.lisa:notion-access operation: create-page with:
{ "parent_database_id": "<PRD_DB>",
"properties": { "<title-prop>": { "title": [{ "text": { "content": "<TITLE>" } }] },
"<STATUS_PROP>": { "status": { "name": "<ROLE_VALUE>" } } },
"children": [ <marker block>, <PRD body blocks> ] }
Use the DB's actual title property name (read it via operation: read-database id: <PRD_DB> if
unknown) and the correct property type for $STATUS_PROP (status vs select).UPDATE (existing page or source_ref):
lisa:notion-access operation: write-page payload: { "id": "<page-id>", "properties": { "<STATUS_PROP>": { "status": { "name": "<ROLE_VALUE>" } } } } — unless the page's current Status is in the resolved ${PROGRESSED[@]} set (already past ready), in which case leave the Status and report reused (already past ready).operation: archive-page is page-level, so for blocks delete via the blocks API through
notion-access or, where block deletion isn't available, replace their text in place) and
operation: append-blocks the regenerated blocks. Do not duplicate the whole spec as a dated
note, and never drop the marker or an existing managed ## Lisa Usage section.ref: "<notion-page-id>"
url: "<page url>"
role: draft | ready # (or the page's current Status role when reused past ready)
marker: "<MARKER>"
outcome: created | reused
lisa:notion-access; never touch the Notion API/MCP directly.## Lisa Usage section on update; never append a second usage
section or silently drop ledger rows.ready.notion.statusProperty, notion.values.*) — never
hardcode value names.documentation
Onboard a user to the project via its LLM Wiki. Interviews the user about themselves in relation to the project, captures that to project-scoped memory only, then gives a guided tour of what the project is and sample questions they can ask. Use when someone is new to the project or asks to be onboarded. Read-mostly — it does not open PRs or write PII into the wiki.
documentation
Migrate an existing, hand-rolled wiki implementation onto the lisa-wiki kernel — phased and compatibility-first, with a strict no-loss guarantee. Use when adopting lisa-wiki in a repo that already has its own wiki/, ingest skills, docs, or roles. Renaming things into the canonical shape is fine; losing functionality or data is not. Ends by running /doctor.
development
Health-check the LLM Wiki. Reports orphan pages, contradictions, stale claims, broken internal links, missing index/log coverage, structure-manifest violations, and secret/tenant leaks. Use periodically or before hardening a wiki. Read-only — it reports findings, it does not fix them.
testing
Ingest source material into the LLM Wiki. With an argument (URL, file path, or prompt) it ingests that one source; with no argument it runs a full ingest across every enabled non-external-write source. Routes to the right connector, then runs the ordered pipeline (source note → synthesis → index → log → verify → state → commit/PR). Use whenever new knowledge should enter the wiki.