plugins/lisa-copilot/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.development
Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.
development
Guidelines for upgrading Expo SDK versions and fixing dependency issues
development
Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).
tools
`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.