SRS Walkthrough
End-to-end tutorial : enable SRS on a project, draft an Epic from scratch, spawn the Stories. Takes about 15 minutes if you already have a Notion integration set up.
New to the model? Start with One source of truth for product requirements to understand what remains in Notion, what the agent may propose, and where explicit approval is required.
What you'll need
- A SaaSFoundryAI project (generated by
sf new) or an existing project where.saasfoundry.jsonlives - A Notion workspace you can create pages in
- A Notion integration token (see
sf-tool-notionfor the one-time setup) - A parent page in Notion that you've explicitly shared with the integration — this is where your SRS root will live
Why a dedicated parent page?
The Notion integration can only see pages it's been shared with. Creating a dedicated "SRS root" page (shared once) is cleaner than sharing the whole workspace — it scopes the integration's reach to exactly the area you want under version.
1. Enable SRS on the project
If you're at sf new time, pass the flags :
sf new --non-interactive \
--project-name tutorial-saas \
--structure monorepo \
--setup-repo local \
--db-setup docker \
--db-type postgresql \
--email-service none \
--no-analytics \
--advanced-skills notion \
--srs-enable \
--srs-backend notion \
--srs-parent-page-input "https://www.notion.so/your-workspace/SRS-root-abc123"On an existing project :
sf update --add-modules srs \
--srs-backend notion \
--srs-parent-page-input "https://www.notion.so/your-workspace/SRS-root-abc123" \
--notion-api-token "secret_..."Either path writes tools.srs into .saasfoundry.json and installs the sf-srs + sf-tool-notion skills under .claude/skills/.
Verify :
jq '.tools.srs' .saasfoundry.jsonExpected :
{
"enabled": true,
"backend": "notion",
"rootPage": {
"id": "abc123-...",
"url": "https://www.notion.so/your-workspace/SRS-root-abc123",
"name": "SRS root"
}
}Smoke-test the adapter :
.claude/skills/sf-srs/scripts/srs-cli.sh validate
# → exit 0 : adapter.init() succeeded2. Create the drafting ticket
SRS work is always driven by a GitHub ticket — that's how the lifecycle transitions get audited. Create one with the srs:new label :
gh issue create \
--title "Epic — Authentication & session management" \
--body "First cut at the auth SRS. Will drive the signup / login / SSO feature tree." \
--label "srs:new,complexity: medium"Note the ticket number that comes back — this example assumes #42.
Drive it through the standard early phases :
.claude/skills/sf-workflow/workflow-cli.sh update-status 42 "Ready"
.claude/skills/sf-workflow/workflow-cli.sh update-status 42 "In progress"3. Brainstorm in conversation
In your Claude Code session, open the ticket and brainstorm with Claude :
Let's draft an Epic for Authentication. I want three FRs : password login, SSO (Google + GitHub), and session refresh. Each needs URs describing what the user sees, DS for the implementation, and TCs for acceptance. No existing notes — draft from scratch.
Claude reads the brainstorm, composes a DraftCandidate[] list (one Epic + N FR specs), and shows it back for approval before writing anything to Notion.
4. Run the drafter
Once the spec reads right :
.claude/skills/sf-workflow/workflow-cli.sh transition-drafting 42 ai-draftClaude then saves the DraftCandidate[] to a temp JSON file and runs :
.claude/skills/sf-srs/scripts/srs-cli.sh write --spec /tmp/draft-42.jsonOn success :
- An Epic page Authentication & session management is created under the SRS root
- Three FR child pages land under the Epic (
FR-001 Password login,FR-002 SSO,FR-003 Session refresh) - Each FR page ships with its canonical sections : User Requirements · Functional Requirements · Design · Test Cases
- The traceability table on the Epic page lists all FRs
- Exit 0 —
tools.srs.pendingIngestionis cleared if it was set
Open the Epic page in Notion — this is your single source of truth from here on.
5. Human review
.claude/skills/sf-workflow/workflow-cli.sh transition-drafting 42 human-reviewGo through each FR page :
- Does the User Requirement match what a product reviewer would write ?
- Do the Design blocks reflect your actual implementation plan (libraries, algorithms, boundaries) ?
- Are the Test Cases specific enough to be runnable tests ?
Edit directly in Notion. The drafter is not re-run for small tightenings — you own the page now.
Tip : comment on the ticket as you review so the audit trail captures what changed between the ai-draft output and the approved page.
6. Spawn the Stories
Once the page tree is approved :
.claude/skills/sf-workflow/workflow-cli.sh transition-drafting 42 spawningClaude runs :
.claude/skills/sf-srs/scripts/srs-cli.sh spawn --ticket 42 --epic "https://www.notion.so/your-workspace/Authentication-..."This enumerates the three FR pages and creates three GitHub sub-issues under ticket #42 :
#43 [Parent #42] FR-001: Password login· labelsrs:new· statusBacklog#44 [Parent #42] FR-002: SSO (Google + GitHub)· labelsrs:new· statusBacklog#45 [Parent #42] FR-003: Session refresh· labelsrs:new· statusBacklog
Use --dry-run first if you want to preview without writing :
.claude/skills/sf-srs/scripts/srs-cli.sh spawn --ticket 42 --epic "..." --dry-runEach spawned ticket's body is rendered from renderStoryTicketBody : a link back to the FR page, the acceptance criteria pulled from the TC blocks, and the traceability chain UR → FR → DS → TC.
7. Close the drafter
.claude/skills/sf-workflow/workflow-cli.sh transition-drafting 42 doneTicket #42 closes on GitHub. The three spawned Stories stay open — they'll flow through the normal code-path workflow from here, each independently picked up, implemented, tested, reviewed, merged.
8. Evolve the spec as you code
Later, during implementation of (say) #43 Password login, you realise a password policy detail wasn't in the SRS. You mention it in conversation :
Passwords should accept any unicode character — we saw a bug with emoji passwords last week.
Claude recognises this as an acceptance condition (TC) on FR-001 and proposes a diff :
💡 Ce que tu viens de décrire ressemble à TC. Proposition :
• FR-001 Password login — add TC-007 : "passwords containing unicode/emoji characters are accepted without error"
J'applique via `srs-cli.sh apply-update` ? [accept / edit / reject]On accept, Claude calls :
echo '{
"kind": "add-tc",
"pageId": "<FR-001 page id>",
"item": {
"id": "TC-007",
"title": "passwords containing unicode/emoji characters are accepted without error"
}
}' | .claude/skills/sf-srs/scripts/srs-cli.sh apply-updateThe FR-001 page now has a new "Added Test Case — TC-007" block appended at the end. Reviewers fold it back into the canonical "Test Cases" section during the next SRS review.
Drafting from an existing codebase
If the project already has code (controllers, Prisma models, React pages, specs, docs) and Notion is empty or stale, skip the brainstorm in section 3 and let the scanners propose the first draft from the source tree.
.claude/skills/sf-srs/scripts/srs-cli.sh draft --from codebase
# or point at a specific repo :
.claude/skills/sf-srs/scripts/srs-cli.sh draft --from codebase --path /path/to/repoThe CLI walks the tree (respecting .gitignore) and emits a JSON envelope :
{
"source": "codebase",
"findings": [
{ "kind": "endpoint", "area": "auth", "method": "POST", "path": "/auth/signin", "hasTests": true, "file": "api/src/modules/auth/auth.controller.ts" },
{
"kind": "entity",
"area": "auth",
"model": "Session",
"fields": [
/* … */
],
"relations": [{ "field": "user", "target": "User" }],
"file": "api/prisma/schema/auth.prisma"
},
{ "kind": "ui-flow", "area": "auth", "title": "SignInPage (public/SignInPage)", "route": "/signin", "linkedEndpointGuess": "signin", "file": "web/src/pages/public/SignInPage.tsx" },
{
"kind": "test",
"area": "auth",
"describe": "AuthService",
"cases": ["hashes passwords with bcrypt", "rejects expired refresh tokens"],
"file": "api/src/modules/auth/tests/unit/auth.service.spec.ts"
},
{ "kind": "doc-context", "area": "project", "heading": "Authentication", "excerpt": "Email + password sign-in with refresh tokens.", "file": "docs/auth.md" }
/* … */
]
}Five scanner kinds fire today — see Scanner findings reference for the full JSON shape of each.
Claude then drives a review loop one domain at a time. The scanner output groups findings by area, so the conversation reads like :
🔍 Findings cluster for `auth` — 6 endpoints, 2 entities, 2 specs, 1 page, 1 doc block.
📘 Epic proposal : Authentication & session management
├─ FR-001 Password sign-in (POST /auth/signin + SignInPage + auth.service.spec.ts)
├─ FR-002 Sign-up (POST /auth/signup + SignUpPage)
└─ FR-003 Session refresh (GET /auth/me + Session model)
Accept this Epic structure, or tighten the FR split first? [accept / edit / reject / skip-area]Before asking for accept/edit, Claude emits a five-category coverage table so the reviewer sees what got seeded per section and where it came from — UR/FR come from endpoints and docs, DS comes from entities and API contracts, TC comes from test.cases[] (plus TODO items for endpoints without tests), NFR is proposed from stack signals and always marked proposed — needs human validation. See Scanner findings reference → Five-category section seeding for the full mapping.
On accept, Claude serialises the cluster as DraftCandidate[] and calls srs-cli.sh write --spec <tmp.json> exactly like the green-field flow in section 4. You can drive multiple Epics in a row, one prompt per cluster — the reviewer always gets a chance to course-correct before any Notion write happens.
Use this flow when :
- You want to bootstrap SRS on a mature project and the codebase is the truth of record
- Your Notion SRS has drifted and you want a fresh baseline from the code
- You want to audit coverage gaps (endpoints without tests, pages without linked endpoints — the scanner marks both)
Troubleshooting
srs-cli.sh validate exits with code 5
Network error during adapter.init(). Check :
NOTION_API_TOKENis set and not a placeholder- The parent page in
tools.srs.rootPage.urlis explicitly shared with the integration (Notion's permission model is opt-in) - Outbound HTTPS to
api.notion.comis allowed
Spawning fails with "Rule 8" error on create-subtask
This is the guard — you're trying to create a Story that doesn't come from the spawner. Either :
- Run
srs-cli.sh spawn --ticket <parent> --epic <url>(the spawner passes--bypass-srs spawned-from-srsautomatically) - Or, for genuinely meta tickets (SRS tooling, drafter refactors), pass
--bypass-srs <your-reason>tocreate-subtaskyourself
The Traceability table on the Epic page is out of date
Known limitation in v1 — adapter.updatePage is append-only on Notion, so the conversational eval hook can't refresh the table in place. When you add an FR through apply-update, the new FR page is created but the Epic's Traceability table isn't edited. Refresh it manually during the next human review, or wait for the replace / delete adapter extension on the roadmap.
What's next
- SRS lifecycle — deep dive on the six drafter phases
- SRS module overview — full reference for flags, config, troubleshooting
- Workflow complexity system — pick the right complexity for each spawned Story
- Skills System →
sf-tool-notion— Notion credentials setup