Back to Open Notebook

Change Playbooks

docs/7-DEVELOPMENT/change-playbooks.md

1.10.010.0 KB
Original Source

Change Playbooks

Step-by-step guides for common types of changes in the Open Notebook codebase. Each playbook lists the files to touch in order, what to do at each step, and what to test.

For AI agents: Read the relevant playbook BEFORE implementing. Follow the sequence — skipping steps causes incomplete changes that break other layers.


How to Use This Document

  1. Identify what type of change your issue requires
  2. Follow the playbook step by step
  3. If a change spans multiple types (e.g., new field + new endpoint), combine the relevant playbooks
  4. When in doubt, read existing examples in the codebase — look at the most recent similar change via git log

Playbook: Add a Field to an Existing Model

Example: "Add language field to Source"

StepFile(s)What to Do
1open_notebook/domain/<model>.pyAdd field with type hint and default value. Follow existing patterns in the class.
2migrations/NNN_<description>.surqlCreate migration. Use next number in sequence. DEFINE FIELD for new fields, UPDATE for backfilling existing records.
3api/models.pyAdd field to *Create, *Update (Optional), and *Response schemas.
4frontend/src/lib/types/api.tsAdd field to the corresponding TypeScript interface (*Response, Create*Request, Update*Request).
5Frontend component (if user-facing)Display or edit the field in the relevant component.
6frontend/src/lib/locales/*/Add i18n strings if the field has a user-visible label. All 7 locales.
7TestsAdd/update tests covering the new field — at minimum, API test for create/read.

Verify: Restart API (migration auto-runs), check logs for migration success, test via /docs.


Playbook: New API Endpoint

Example: "Add endpoint to export notebook as PDF"

StepFile(s)What to Do
1api/models.pyDefine request/response Pydantic schemas. Naming: <Feature>Request, <Feature>Response.
2api/routers/<resource>.pyAdd endpoint to existing router, OR create new router file if it's a new resource. Follow the pattern: validate → call service → return response.
3api/<resource>_service.pyBusiness logic goes here, not in the router. Create new service file if needed.
4api/main.pyIf new router file: register with app.include_router().
5frontend/src/lib/types/api.tsAdd TypeScript types matching the Pydantic schemas.
6frontend/src/lib/api/<resource>.tsAdd method to the API module. Follow existing pattern (axios call, return response.data).
7frontend/src/lib/hooks/use-<resource>.tsAdd React Query hook. useQuery for GET, useMutation for POST/PUT/DELETE. Include cache invalidation and toast.
8Frontend component/pageWire up the hook in the UI.
9TestsAPI test (status codes, validation, error cases).

Naming conventions:

  • Routers: @router.get("/resources/{id}") (plural, lowercase, kebab for multi-word)
  • Services: functions are async, named descriptively (process_source, generate_podcast)
  • Hooks: useResources() for list, useResource(id) for single, useCreateResource() for mutation

Playbook: New LangGraph Workflow

Example: "Add a summarization workflow"

StepFile(s)What to Do
1prompts/<workflow_name>/*.jinjaCreate Jinja2 prompt templates. Use Prompter from ai-prompter.
2open_notebook/graphs/<workflow_name>.pyDefine StateDict (TypedDict), node functions, build graph with StateGraph. Use provision_langchain_model() for model selection. Wrap LLM calls with classify_error().
3api/<resource>_service.pyInvoke graph: await graph.ainvoke(state, config).
4api/routers/<resource>.pyExpose endpoint to trigger the workflow.
5commands/<workflow>_commands.pyIf the workflow should run async: create command with CommandInput/CommandOutput. Register in command service.
6Frontend integrationAPI module → hook → component.
7TestsTest graph nodes individually with mocked LLM responses.

Key patterns:

  • Nodes are sync functions (LangGraph requirement) but can call async code via ThreadPoolExecutor
  • Use classify_error() to convert raw exceptions to typed OpenNotebookError subclasses
  • Use provision_langchain_model() for model selection — never hardcode a provider
  • State is a TypedDict, NOT a Pydantic model

Playbook: Bug Fix (Single Layer)

Example: "order_by parameter not working on sources endpoint"

StepWhat to Do
1Identify the layer. Read the issue and determine: frontend, API router, service, domain model, database, or graph.
2Read the module's CLAUDE.md. Every major module has one. It documents patterns and gotchas.
3Reproduce. Use the API docs (/docs), browser, or a test to confirm the bug.
4Fix. Make the minimal change needed. Don't refactor surrounding code.
5Add a test that reproduces the bug and verifies the fix.
6Run existing tests to verify no regression: uv run pytest tests/

Playbook: Bug Fix (Cross-Layer)

Example: "Creating a source via URL doesn't show in notebook"

StepWhat to Do
1Trace the data flow. Start from where the user sees the problem (frontend) and trace backward: component → hook → API call → router → service → domain → database.
2Identify where the chain breaks. Use API docs to test the backend independently of the frontend. Use SurrealDB queries to check if data was persisted.
3Fix at the right layer. Don't patch the symptom in the frontend if the bug is in the service.
4Verify the full chain after fixing.
5Add tests at the layer where the bug was.

Playbook: Database Migration

Example: "Add index on source.notebook_id for query performance"

StepFile(s)What to Do
1migrations/NNN_<description>.surqlWrite SurrealQL. Use next number in sequence. Check existing migrations for patterns.
2Domain model (if schema change)Update field definitions to match.
3API schemas (if new/changed fields)Update Pydantic models.
4Verify: Restart API and check logsMigrations auto-run on startup. Look for errors in Loguru output.

Important:

  • Migrations are numbered and run in order
  • They're tracked in _migrations table — won't re-run
  • For destructive changes (DROP FIELD), consider data preservation
  • Test with existing data, not just empty database

Playbook: Frontend-Only Change

Example: "Improve notebook list loading state"

StepFile(s)What to Do
1Identify componentComponents are in frontend/src/app/ (pages) or frontend/src/components/ (shared).
2Make changesFollow existing patterns: functional components, hooks for state, Tailwind for styling.
3i18n stringsIf adding user-visible text, add to ALL locale files under frontend/src/lib/locales/.
4Test in browserCheck responsive layout, dark mode (if applicable), loading states, empty states, error states.

Key patterns:

  • 'use client' directive at top of components using hooks
  • State: useState for local, Zustand for global, TanStack Query for server
  • Styling: Tailwind utility classes, Shadcn/ui components from components/ui/
  • Types: Define in lib/types/api.ts, import everywhere

Playbook: New Background Command

Example: "Add command to rebuild all embeddings for a notebook"

StepFile(s)What to Do
1commands/<name>_commands.pyDefine CommandInput and CommandOutput Pydantic classes. Write the command function.
2Register commandAdd to the command service so it can be submitted via CommandService.submit_command_job().
3API endpointAdd endpoint that submits the command and returns the command ID.
4Frontend (polling)Use /commands/{command_id} endpoint to poll for status. Show progress to user.

Pattern:

  • Commands are fire-and-forget: submit returns immediately with a command ID
  • Retry config: max_attempts, stop_on exceptions (ValueError = no retry)
  • Exponential backoff with jitter for transient failures

Playbook: i18n / Translation Update

Example: "Add translations for new settings page"

StepFile(s)What to Do
1frontend/src/lib/locales/en-US/index.tsAdd English strings first. Group by feature.
2All other locale filesAdd the same keys to: pt-BR, zh-CN, zh-TW, ja-JP, ru-RU, bn-IN. Use English as placeholder if translation unavailable.
3ComponentUse const { t } = useTranslation() and access via t.section.key.

7 locales total. Don't forget any.


Quick Reference: File Locations by Layer

LayerLocationSchema/TypesTests
Domain modelsopen_notebook/domain/Pydantic fieldstests/
Databaseopen_notebook/database/repository.pySurrealQLtests/
Migrationsmigrations/*.surqlSurrealQLAuto-run on startup
AI/LLMopen_notebook/ai/Esperanto typestests/
Graphsopen_notebook/graphs/TypedDict statetests/
Promptsprompts/**/*.jinjaJinja2 context
Commandscommands/CommandInput/Outputtests/
API routersapi/routers/api/models.pytests/
API servicesapi/*_service.pytests/
Frontend typesfrontend/src/lib/types/TypeScript interfaces
Frontend APIfrontend/src/lib/api/
Frontend hooksfrontend/src/lib/hooks/frontend/src/test/
Frontend componentsfrontend/src/components/Props interfacesfrontend/src/test/
Frontend pagesfrontend/src/app/
i18nfrontend/src/lib/locales/