将工作流描述转换为展示UI和代码功能的表格,并显示其连接关系。用于映射现有系统或设计新系统。
Breadboarding transforms a workflow description into a complete map of affordances and their relationships. The output is always a set of tables showing numbered UI and Code affordances with their Wires Out and Returns To relationships. The tables are the truth. Mermaid diagrams are optional visualizations for humans.
Breadboarding serves two functions:
You don't understand how an existing system works in its concrete details. You have a workflow you're trying to understand — explaining how something happens or why something doesn't happen.
Input:
Output:
Note: If the workflow spans multiple applications (frontend + backend), create ONE breadboard that tells the full story. Label places to show which system they belong to.
You have a new system sketched as an assembly of parts (mechanisms) per shaping. You need to detail out the concrete mechanism and show how those parts interact as a system.
Input:
Output:
Often you have both: an existing system that must remain as-is, plus new pieces or changes defined in a shape. In this case, breadboard both together — the existing affordances and the new ones — showing how they connect.
Hand-drawn or whiteboard breadboards use a visual stacking format rather than tables. The same concepts apply (Places, affordances, wiring) but the layout conventions differ.
Visual conventions:
ElementHow it appearsPlaceColored block (often pink/purple) at the top of a vertical stackAffordances in a placeBlocks stacked underneath the place block — containment is shown by vertical position in the stackCode affordancesTypically float between place stacks, not inside themPlace loaderA code affordance positioned at the top-left of the place block — describes the data/inputs needed to render that placeWires OutSolid arrows between blocksReturns ToDashed arrows between blocksConditionalsIndented blocks within a stack, often a different color (e.g., green), showing if/else branchesPlace references_ prefix on a place name within a stack (same as _PlaceName convention)Uncertain/tentative? prefix or ~ prefix on an affordance name, or dashed borders — indicates the affordance is speculativeContaining boxA large boundary drawn around multiple stacks — groups affordances by system or responsibility boundary (e.g., "HireEZ" box)Notes/annotationsFreeform text near elements — context, open questions, or rationale
How to read a whiteboard breadboard:
? and ~ prefixed items are uncertain and may not survive shapingTranslating to tables: When converting a whiteboard breadboard to standard affordance tables, map each stack to its Place, enumerate the affordances top-to-bottom, and capture the arrows as Wires Out / Returns To relationships. Loaders become code affordances with Returns To pointing at the UI affordances they feed.
A Place is a bounded context of interaction. While you're in a Place:
Place is perceptual, not technical. It's not about URLs or components — it's about what the user experiences as their current context. A Place is "where you are" in terms of what you can do right now.
The simplest test for whether something is a different Place: Can you interact with what's behind?
AnswerMeaningNoYou're in a different PlaceYesSame Place, with local state changes
UI ElementBlocking?Place?WhyModalYesYesCan't interact with page behindConfirmation popoverYesYesMust respond before returning (limit case of modal)Edit mode (whole screen transforms)YesYesAll affordances changedCheckbox reveals extra fieldsNoNoSurroundings unchangedDropdown menuNoNoCan click away, non-blockingTooltipNoNoInformational, non-blocking
When a control changes state, ask: did everything change, or just a subset while the surroundings stayed the same?
TypeWhat happensHow to modelLocal stateSubset of UI changes, surroundings unchangedSame Place, conditional N → dependent UsPlace navigationEntire screen transforms, or blocking overlayDifferent Places
When a mode (like "edit mode") transforms the entire screen — different buttons, different affordances everywhere — model as separate Places:
text
PLACE: CMS Page (Read Mode)
PLACE: CMS Page (Edit Mode)
The state flag (e.g., editMode$) that switches between them is a navigation mechanism, not a data store. Don't include it as an S in either Place.
For any UI affordance, ask:
If the answer to #3 is "everything changes" or "I can't interact with what's behind until I respond," that's navigation to a different Place.
PatternUsePLACE: Page NameStandard page/routePLACE: Page Name (Mode)Mode-based variant of a pagePLACE: Modal NameModal dialogPLACE: BackendAPI/database boundary
When spanning multiple systems, label with the system: PLACE: Checkout Page (frontend), PLACE: Payment API (backend).
Places are first-class elements in the data model. Each Place gets an ID:
#PlaceDescriptionP1CMS Page (Read Mode)View-only stateP2CMS Page (Edit Mode)Editing state with page-level controlsP2.1widget-grid (letters)Subplace: letter editing widget within P2P3Letter Form ModalForm for adding/editing lettersP4BackendAPI resolvers and database
Place IDs enable:
→ P2 instead of to an affordance insideWhen a nested place has lots of internal affordances and would clutter the parent, you can detach it:
_letter-browser_letter-browser --> letter-browserThe reference is a UI affordance — it represents "this widget/component renders here" in the parent context.
Rendering diagram...
In affordance tables, list the reference as a UI affordance:
#AffordanceControlWires OutU1Edit buttonclick→ N1_letter-browserWidget reference—→ P3
Style place references with a dashed border to distinguish them:
text
classDef placeRef fill:#ffb6c1,stroke:#d87093,stroke-width:2px,stroke-dasharray:5 5
class U_LB placeRef
When a component has distinct modes (read vs edit, viewing vs editing, collapsed vs expanded), model them as separate places — they're different perceptual states for the user.
If one mode includes everything from another plus more, show this with a place reference inside the extended place:
text
P3: letter-browser (Read) — base state
P4: letter-browser (Edit) — contains _letter-browser (Read) + new affordances
The reference shows composition: "everything in P3 appears here, plus these additions."
Rendering diagram...
In affordance tables for P4, the reference shows inheritance:
#AffordanceControlWires OutNotes_letter-browser (Read)Inherits all of P3—→ P3U3Add buttonclick→ N3NEWU4Edit buttonclick→ N4NEW
A subplace is a defined subset of a Place — a contained area that groups related affordances. Use subplaces when:
Notation: Use hierarchical IDs — P2.1, P2.2, etc. for subplaces of P2.
text
| # | Place | Description |
|---|-------|-------------|
| P2 | Dashboard | Main dashboard page |
| P2.1 | Sales widget | Subplace: sales metrics |
| P2.2 | Activity feed | Subplace: recent activity |
In affordance tables, use the subplace ID to show containment:
text
| U3 | P2.1 | sales-widget | "Refresh" button | click | → N4 | — |
| U7 | P2.2 | activity-feed | activity list | render | — | — |
In Mermaid: Nest the subplace subgraph inside the parent. Use the same background color (no distinct fill) — the subplace is part of the parent, not a separate Place:
Rendering diagram...
Placeholder for out-of-scope content: When detailing one subplace, add a placeholder sibling to show there's more on the page:
text
otherContent[["... other page content ..."]]
This tells readers: "we're zooming in on P2.1, but P2 contains more that we're not detailing."
These are two different relationships in the data model:
RelationshipMeaningWhere CapturedContainmentAffordance belongs to / lives in a PlacePlace column (set membership)WiringAffordance triggers / calls somethingWires Out column (control flow)
Containment is set membership: U1 ∈ P1 means U1 is a member of Place P1. Every affordance belongs to exactly one Place.
Wiring is control flow: U1 → N1 means U1 triggers N1. An affordance can wire to anything — other affordances or Places.
The Place column answers: "Where does this affordance live?" The Wires Out column answers: "What does this affordance trigger?"
When an affordance causes navigation (user "goes" somewhere), wire to the Place itself, not to an affordance inside:
text
✅ N1 Wires Out: → P2 (navigate to Edit Mode)
❌ N1 Wires Out: → U3 (wiring to affordance inside P2)
This makes navigation explicit in the tables. The Place is the destination; specific affordances inside become available once you arrive.
In Mermaid, this becomes:
text
N1 --> P2
The subgraph ID matches the Place ID, so the wire connects to the Place boundary.
Things you can act upon:
How affordances connect to each other:
Wires Out — What an affordance triggers or calls (control flow):
Returns To — Where an affordance's output flows (data flow):
This separation makes data flow explicit. Wires Out show control flow (what triggers what). Returns To show data flow (where output goes).
The tables are the truth. Every breadboard produces these:
#PlaceDescriptionP1Search PageMain search interfaceP2Detail PageIndividual result view
#PlaceComponentAffordanceControlWires OutReturns ToU1P1search-detailsearch inputtype→ N1—U2P1search-detailloading spinnerrender——U3P1search-detailresults listrender——U4P1search-detailresult rowclick→ P2—
#PlaceComponentAffordanceControlWires OutReturns ToN1P1search-detailactiveQuery.next()call→ N2—N2P1search-detailactiveQuery subscriptionobserve→ N3—N3P1search-detailperformSearch()call→ N4, → N5, → N6—N4P1search.servicesearchOneCategory()call→ N7→ N3N5P1search-detailloadingwritestore→ U2N6P1search-detailresultswritestore→ U3N7P1typesense.servicerawSearch()call—→ N4
#PlaceStoreDescriptionS1P1resultsArray of search resultsS2P1loadingBoolean loading state
ColumnDescription**#Unique ID (P1, P2... for Places; U1, U2... for UI; N1, N2... for Code; S1, S2... for Stores)PlaceWhich Place this affordance belongs to (containment)ComponentWhich component/service owns thisAffordanceThe specific thing you can act uponControlThe triggering event: click, type, call, observe, write, renderWires Out**What this triggers: → N4, → P2 (control flow, including navigation)Returns ToWhere output flows: → N3 or → U2, U3 (data flow)
See Example A below for a complete worked example.
Step 1: Identify the flow to analyze
Pick a specific user journey. Always frame it as an operator trying to do something:
Step 2: List all places involved
Walk through the journey and identify each distinct place the user visits or system boundary crossed.
Step 3: Trace through the code to find components
Starting from the entry point (route, API endpoint), trace through the code to find every component touched by that flow.
Step 4: For each component, list its affordances
Read the code. Identify:
Step 5: Name the actual thing, not an abstraction
If you write "DATABASE", stop. What's the actual method? ( userRepo.save()). Every affordance name must be something real you can point to in the code.
Step 6: Fill in Control column
For each affordance, what triggers it? (click, type, call, observe, write, render)
Step 7: Fill in Wires Out
For each affordance, what does it trigger? Read the code — what does this method call? What does this button's handler invoke?
Step 8: Fill in Returns To
For each affordance, where does its output flow?
—Step 9: Add data stores as affordances
When code writes to a property that is later read by another affordance, add that property as a Code affordance with control type write.
Step 10: Add framework mechanisms as affordances
Include things like cdr.detectChanges() that bridge between code and UI rendering. These show how state changes actually reach the UI.
Step 11: Verify against the code
Read the code again. Confirm every affordance exists and the wiring matches reality.
See Example B below for a complete worked example including slicing.
Step 1: List each part from the shape
Take each mechanism/part identified in shaping and write it down.
Step 2: Translate parts into affordances
For each part, identify:
Step 3: Verify every U has a supporting N
For each UI affordance, check: what Code affordance provides its data or controls its rendering? If none exists, add the missing N.
Step 4: Classify places as existing or new
For each UI affordance, determine whether it lives in:
Step 5: Wire the affordances
Fill in Wires Out and Returns To for each affordance. Trace through the intended behavior — what calls what? What returns where?
Step 6: Connect to existing system (if applicable)
If there's an existing codebase:
Step 7: Check for completeness
Step 8: Treat user-visible outputs as Us
Anything the user sees (including emails, notifications) is a UI affordance and needs an N wiring to it.
When tracing a flow backwards, don't follow the path you remember. Scan the Wires Out column for ALL affordances that wire to your target.
When filling in the tables, read each row systematically. Don't rely on what you think you know.
The tables are the source of truth. Your memory is unreliable.
When mapping existing code, never invent abstractions. Every name must point to something real in the codebase.
An affordance is something you can act upon that has meaningful identity in the system. Several things look like affordances but are actually just implementation mechanisms:
TypeExampleWhy it's not an affordanceVisual containersmodal-frame wrapperYou can't act on a wrapper — it's just a Place boundaryInternal transformsletterDataTransform()Implementation detail of the caller — not separately actionableNavigation mechanismsmodalService.open()Just the "how" of getting to a Place — wire to the destination directly
These aren't always obvious on first draft. When reviewing your affordance tables, double-check each Code affordance and ask:
"Is this actually an affordance, or is it just detailing the mechanism for how something happens?"
If it's just the "how" — skip it and wire directly to the destination or outcome.
Examples:
text
❌ N8 --> N22 --> P3 (N22 is modalService.open — just mechanism)
✅ N8 --> P3 (handler navigates to modal)
❌ N6 --> N20 --> S2 (N20 is data transform — internal to N6)
✅ N6 --> S2 (callback writes to store)
❌ U7: modal-frame (wrapper — just the boundary of P3)
✅ U8: Save button (actionable)
The handler navigates to P3. The callback writes to the store. The modal IS P3. The mechanisms are implicit.
A breadboard captures two distinct flows:
FlowWhat it tracksWiringNavigationMovement from Place to PlaceWires Out → PlacesDataHow state populates displaysReturns To → Us
These are orthogonal. You can have navigation without data changes, and data changes without navigation.
When reviewing a breadboard, trace both flows:
A UI affordance that displays data must have something feeding it — either a data store (S) or a code affordance (N) that returns data.
text
❌ U6: letter list (no incoming wire — where does the data come from?)
✅ S1 -.-> U6 (store feeds the display)
✅ N4 -.-> U6 (query result feeds the display)
If a display U has no data source wiring into it, either:
This is easy to miss when focused on navigation. Always ask: "This U shows data — where does that data come from?"
If a Code affordance has no Wires Out AND no Returns To, something is wrong:
An N that appears to wire nowhere is suspicious. If it has side effects outside the system boundary (browser URL, localStorage, external API, analytics), add a store node to represent that external state:
text
❌ N41: updateUrl() (wires to... nothing?)
✅ N41: updateUrl() → S15 (wires to Browser URL store)
This makes the data flow explicit. The store can also have return wires showing how external state flows back in:
Rendering diagram...
Common external stores to model:
Browser URL — query params, hash fragmentslocalStorage / sessionStorage — persisted client stateClipboard — copy/paste operationsBrowser History — navigation stateWires Out = control flow (what triggers what) Returns To = data flow (where output goes)
This separation makes the system's behavior explicit.
Routing is a generic mechanism every page uses. Instead of drawing all navigation through a central Router affordance, show Router navigate() inline where it happens and wire directly to the destination place.
A data store belongs in the Place where its data is consumed to enable some effect — not where it's produced. Writes from other Places are "reaching into" that Place's state.
To determine where a store belongs:
Example: A changedPosts array is written by a Modal (when user confirms changes) but read by a PAGE_SAVE handler (when user clicks Save). The store belongs with the PAGE_SAVE handler — that's where it enables the persistence operation.
Before putting a store in a separate DATA STORES section, verify it's actually read by multiple Places. If it only enables behavior in one Place, it belongs inside that Place.
Within a Place, put stores in the subcomponent where they enable behavior. If a store is read by a specific handler, put it in that handler's component — not floating at the Place level.
The database and resolvers aren't floating infrastructure — they're a Place with their own affordances. Database tables (S) belong inside the Backend Place alongside the resolvers (N) that read and write them.
This section provides a complete reference of everything that can appear in a breadboard.
ElementID PatternWhat It IsWhat QualifiesPlaceP1, P2, P3...A bounded context of interactionBlocking test: can't interact with what's behindSubplaceP2.1, P2.2...A defined subset within a PlaceGroups related affordances within a larger PlacePlace Reference_PlaceNameUI affordance pointing to a detached placeComplex nested place defined separatelyUI AffordanceU1, U2, U3...Something the user can see or interact withInputs, buttons, displays, scroll regionsCode AffordanceN1, N2, N3...Something in code you can act uponMethods, subscriptions, handlers, framework mechanismsData StoreS1, S2, S3...State that persists and is read/writtenProperties, arrays, observables that hold dataChunk—A collapsed subsystemOne wire in, one wire out, many internalsPlaceholder—Out-of-scope content markerShows context without detailing
RelationshipSyntaxMeaningExampleContainmentPlace columnAffordance belongs to PlaceU3 in Place P2.1Wires Out→ XControl flow: triggers/calls→ N4, → P2Returns To→ X (in Returns To column)Data flow: output goes to→ U6, → N3Abbreviated flow|label|Intermediate steps omittedS4 -.-> |view query| U6Parent-childHierarchical IDSubplace belongs to PlaceP2.1 is child of P2
RelationshipMeaningWhere CapturedContainmentAffordance belongs to / lives in a PlacePlace column (set membership)WiringAffordance triggers / calls somethingWires Out column (control flow)
Containment is set membership: U1 ∈ P1 means U1 is a member of Place P1.
Wiring is control flow: U1 → N1 means U1 triggers N1.
Place (P):
Place Reference (_PlaceName):
_letter-browser, _user-profile-widget_letter-browser --> P3UI Affordance (U):
Code Affordance (N):
handleSubmit(), query$ subscription, detectChanges()Data Store (S):
results array, loading boolean, changedPosts listBrowser URL, localStorage, Clipboard — represent state outside the app boundaryCheckQuestionIf No...Every U that displays dataDoes it have an incoming wire (via Wires Out or Returns To)?Add the data sourceEvery NDoes it have Wires Out or Returns To (or both)?Investigate — may be dead code or missing wiringEvery SDoes something read from it (Returns To)?Investigate — may be unusedNavigation mechanismsIs this N just the "how" of getting somewhere?Wire directly to Place insteadN with side effectsDoes this N affect external state (URL, storage, clipboard)?Add a store for the external state
Chunking collapses a subsystem into a single node in the main diagram, with details shown separately. Use chunking to manage complexity when a section of the breadboard has:
Look for sections where tracing the wiring reveals a "pinch point" — many affordances that funnel through a single input and single output. These are natural boundaries for chunking.
Example: A dynamic-form component receives a form definition, renders many fields (U7a-U7k), validates on change (N26), and emits a single valid$ signal. In the main diagram, this becomes:
text
N24 -->|formDefinition| dynamicForm
dynamicForm -.->|valid$| U8
text
dynamicForm[["CHUNK: dynamic-form"]]
text
N24 -->|formDefinition| dynamicForm
dynamicForm -.->|valid$| U8
Rendering diagram...
text
classDef chunk fill:#b3e5fc,stroke:#0288d1,color:#000,stroke-width:2px
class dynamicForm chunk
TypeColorHexChunk node (main diagram)Light blue#b3e5fcBoundary markers (chunk diagram)Light blue, dashed#b3e5fc with stroke-dasharray:5 5
The tables are the truth. Mermaid diagrams are optional visualizations for humans.
Rendering diagram...
Line StyleMermaid SyntaxUseSolid ( -->)A --> BWires Out: calls, triggers, writesDashed ( -.->)A -.-> BReturns To: return values, data store readsLabeled ...`A -.->...
When a data flow has intermediate steps that aren't relevant to the breadboard's scope, abbreviate by wiring directly from source to destination with a ... label:
text
S4 -.->|...| U6
This says "data flows from S4 to U6, with intermediate steps omitted." Use this when:
PrefixTypeExamplePPlacesP1, P2, P3UUI affordancesU1, U2, U3NCode affordancesN1, N2, N3SData storesS1, S2, S3
TypeColorHexPlaces (subgraphs)White/transparent—UI affordancesPink#ffb6c1Code affordancesGrey#d3d3d3Data storesLavender#e6e6faChunksLight blue#b3e5fcPlace referencesPink, dashed border#ffb6c1text
classDef ui fill:#ffb6c1,stroke:#d87093,color:#000
classDef nonui fill:#d3d3d3,stroke:#808080,color:#000
classDef store fill:#e6e6fa,stroke:#9370db,color:#000
classDef chunk fill:#b3e5fc,stroke:#0288d1,color:#000,stroke-width:2px
classDef placeRef fill:#ffb6c1,stroke:#d87093,stroke-width:2px,stroke-dasharray:5 5
Use the Place ID as the subgraph ID so navigation wiring connects properly:
Rendering diagram...
TypeID PatternLabel PatternPurposePlaceP1, P2...P1: Page NameA bounded context the user visitsTrigger—TRIGGER: NameAn event that kicks off a flow (not navigable)Component—COMPONENT: NameReusable UI+logic that appears in multiple placesSystem—SYSTEM: NameWhen spanning multiple applications
Key point: The subgraph ID ( P1, P2) must match the Place ID from the Places table. This allows navigation wires like N1 --> P2 to connect to the Place boundary.
Rendering diagram...
When breadboarding a specific workflow, you can optionally add numbered step markers to help readers follow the sequence visually. This is useful when:
Format:
Add a Workflow Guide table before the diagram:
markdown
| Step | Action | Where to look |
|------|--------|---------------|
| **1** | Click "Edit" button | U1 → N1 → S1 |
| **2** | Edit mode activates | S1 → N2 → U3 |
| **3** | Click "Add" | U3 → N3 → N8 |
Add step marker nodes in the Mermaid diagram using stadium-shaped nodes:
Rendering diagram...
Formatting notes:
"1 - ACTION" format (number, space, hyphen, space, action)"1. ACTION" — the period triggers Mermaid's markdown list parser"1) ACTION" — parentheses can also cause parsing issues-.->)Slicing takes a breadboard and groups its affordances into vertical implementation slices. See Example B below for a complete slicing example.
Input:
Output:
A vertical slice is a group of UI and Code affordances that does something demo-able. It cuts through all layers (UI, logic, data) to deliver a working increment.
The opposite is a horizontal slice — doing work on one layer (e.g., "set up all the data models") that isn't clickable from the interface.
Every slice must have visible UI that can be demoed. A slice without UI is a horizontal layer, not a vertical slice.
Demo-able means:
The shape guides what counts as "meaningful progress" — you're not just grouping affordances arbitrarily, you're grouping them to demonstrate mechanisms working.
Aim for ≤9 slices. If you need more, the shape may be too large for one cycle.
A slice may contain affordances with Wires Out pointing to affordances in later slices. These wires exist in the breadboard but aren't implemented yet — they're stubs or no-ops until that later slice is built.
This is normal. The breadboard shows the complete system; slicing shows the order of implementation.
Step 1: Identify the minimal demo-able increment
Look at your breadboard and shape. Ask: "What's the smallest subset that demonstrates the core mechanism working?"
Usually this is:
This becomes V1.
Step 2: Layer additional capabilities as slices
Look at the mechanisms in your shape. Each slice should demonstrate a mechanism working:
Max 9 slices. If you have more, combine related mechanisms. Features that don't make sense alone should be in the same slice.
Step 3: Assign affordances to slices
Go through every affordance and assign it to the slice where it's first needed to demo that slice's mechanism:
SliceMechanismAffordancesV1Core displayU2, U3, N3, N4, N5, N6, N7V2SearchU1, N1, N2V3PaginationU10, N11, N12, N13
Some affordances may have Wires Out to later slices — that's fine. They're implemented in their assigned slice; the wires just don't do anything yet.
Step 4: Create per-slice affordance tables
For each slice, extract just the affordances being added:
V2: Search Works
#ComponentAffordanceControlWires OutReturns ToU1search-detailsearch inputtype→ N1—N1search-detailactiveQuery.next()call→ N2—N2search-detailactiveQuery subscriptionobserve→ N3—
Step 5: Write a demo statement for each slice
Each slice needs a concrete demo that shows its mechanism working toward the R:
The demo should be something you can show a stakeholder that demonstrates progress.
Show the complete breadboard in every slice diagram, but use styling to distinguish scope:
CategoryStyleDescriptionThis sliceBright colorAffordances being addedAlready builtSolid greyPrevious slicesFutureTransparent, dashed borderNot yet built
Rendering diagram...
This lets stakeholders see:
#SliceMechanismDemoV1Widget with real dataF1, F4, F6"Widget shows letters from API"V2Search worksF3"Type to filter results"V3Infinite scrollF5"Scroll down, more load"V4URL stateF2"Refresh preserves search"
The Mechanism column references parts from the shape, showing which mechanisms each slice demonstrates.
This example shows breadboarding an existing system to understand how data flows through multiple entry points.
Workflow to understand: "How is admin_organisation_countries modified and read downstream? There are multiple entry points: manual edit, checkbox toggle, and batch job."
UI Affordances
#ComponentAffordanceControlWires OutReturns ToU1SSO Adminrole_profiles checkboxesrender——U2SSO Admin"Country Admin" checkboxclicktoggles selection—U3SSO Adminadmin_countries filter_horizontalrender——U4SSO AdminAvailable countries listrender——U5SSO AdminSelected countries listrender——U6SSO AdminAdd → / Remove ←clickmodifies selection—U7SSO AdminSave buttonclick→ N3—U20DWConnect"Country admins" sectionrender——U21(unknown)System email "From" fieldrender——
Code Affordances
#ComponentAffordanceControlWires OutReturns ToN1sso/accounts/adminget_fieldsets()call→ U3 (conditional)—N2sso/accounts/modelsget_administrable_user_countries()call—→ U4N3sso/accounts/adminsave_form()call→ N4, → N5—N4Django AdminForm M2M savecall→ S2—N5sso/forms/mixins_update_user_m2m()call→ S1, → N6—N6sso/signalsuser_m2m_field_updated signalsignal→ N10—N7CLI/Schedulermanage.py dwbn_cleanupinvoke→ N15—N10sso-dwbn-themedwbn_user_m2m_field_updated()receive→ N11—N11sso-dwbn-themedwbn_user_m2m_field_updated_task()call→ N12—N12sso-dwbn-themeCountry Admin added AND zero admin countries?conditional→ N20—N15sso-dwbn-themeadmin_changes()call→ N16—N16sso-dwbn-themeFor each Country Admin: home center country missing?loop→ N20—N20sso-dwbn-themeGet home center's countrycall→ N21—N21sso-dwbn-themeadmin_organisation_countries.add()call→ S2—N22sso-dwbn-themeupdate_last_modified()call——N30dwconnect2-backendfindCenterAdmins()call—→ U20N31sso/apiget_object_data()call—→ external
Data Stores
#StoreDescriptionS1role_profilesM2M: which role profiles a user hasS2admin_organisation_countriesM2M: which countries a user administersS3organisationsUser's home center(s)
Mermaid Diagram
Rendering diagram...
This section shows what comes FROM shaping — the requirements, existing patterns identified, and sketched parts. This is the INPUT that breadboarding receives.
Note: This example uses shaping terminology. In shaping, you define requirements (Rs), identify existing patterns to reuse, and sketch a solution as parts/mechanisms. Breadboarding takes this shaped solution and details out the concrete affordances and wiring.
The R (Requirements)
IDRequirementR0Make content searchable from the index pageR2Navigate back to pagination state when returning from detailR3Navigate back to search state when returning from detailR4Search/pagination state survives page refreshR5Browser back button restores previous search/pagination stateR9Search should debounce input (not fire on every keystroke)R10Search should require minimum 3 charactersR11Loading and empty states should provide user feedback
Existing System with Reusable Patterns (S-CUR)
The app already has a global search page that implements most of these Rs. During shaping, it was documented at the parts/mechanism level:
PartMechanismS-CUR1****URL state & initializationS-CUR1.1Router queryParams observable provides {q, category}S-CUR1.2initializeState(params) sets query and category from URLS-CUR1.3On page load, triggers initial search from URL stateS-CUR2****Search inputS-CUR2.1Search input binds to activeQuery BehaviorSubjectS-CUR2.2activeQuery subscription with 90ms debounceS-CUR2.3Min 3 chars triggers performNewSearch()S-CUR3****Data fetchingS-CUR3.1performNewSearch() sets loading state, calls search serviceS-CUR3.2Search service builds Typesense filter, calls rawSearch()S-CUR3.3rawSearch() queries Typesense, returns {found, hits}S-CUR3.4Results written to detailResult data storeS-CUR4****PaginationS-CUR4.1Scroll-to-bottom triggers appendNextPage() via intercomServiceS-CUR4.2appendNextPage() increments page, calls searchS-CUR4.3New hits concatenated to existing hitsS-CUR4.4sendMessage() re-arms scroll detectionS-CUR5****RenderingS-CUR5.1cdr.detectChanges() triggers template re-evaluationS-CUR5.2Loading spinner, "no results", result count based on storeS-CUR5.3*ngFor renders tiles for each hitS-CUR5.4Tile click navigates to detail page
Sketched Solution: Parts that Adapt S-CUR
The new solution's parts explicitly reference which S-CUR patterns they adapt:
PartMechanismAdaptsF1Create widget (component, def, register)—F2URL state & initialization (read ?q=, restore on load)S-CUR1F3Search input (debounce, min 3 chars, triggers search)S-CUR2F4Data fetching ( rawSearch() with filter)S-CUR3F5Pagination (scroll-to-bottom, append pages, re-arm)S-CUR4F6Rendering (loading, empty, results list, rows)S-CUR5
This is where breadboarding happens. The shaped parts become concrete affordances with explicit wiring. The output is the affordance tables and diagram.
UI Affordances
#ComponentAffordanceControlWires OutReturns ToU1letter-browsersearch inputtype→ N1—U2letter-browserloading spinnerrender——U3letter-browserno results msgrender——U4letter-browserresult countrender——U5letter-browserresults listrender→ U6, U7, U8, U9—U6letter-rowrow clickclick→ LD—U7letter-rowdaterender——U8letter-rowsubjectrender——U9letter-rowteaserrender——U10letter-browserscrollscroll→ N11—U11browserback buttonclick→ N9—U12letter-browser"See all X results"click→ LP—LD—Letter Detailplace——LP—Full Pageplace——
Code Affordances
#ComponentAffordanceControlWires OutReturns ToN1letter-browseractiveQuery.next()call→ N2→ U12N2letter-browseractiveQuery subscriptionobserve→ N3—N3letter-browserperformSearch()call→ N4, → N6, → N7, → N8—N4typesense.servicerawSearch()call—→ N3, → N12N5letter-browserparentId (config)config—→ N4N6letter-browserloading storewrite—→ N8N7letter-browserdetailResult storewrite—→ N8, → N16N8letter-browserdetectChanges()call→ U2, → U3, → U4, → U5—N9browserURL ?q=read→ N10—N10letter-browserinitializeState()call→ N1, → N3—N11intercom.servicescroll subjectobserve→ N12—N12letter-browserappendNextPage()call→ N4, → N7, → N8, → N13, → N14—N13intercom.servicesendMessage()call→ N11—N14routernavigate()call—→ N9N15letter-browserif !compact subscribeconditional→ N11—N16letter-browserif truncated show linkconditional→ U12—N17letter-browsercompact (config)config—→ N4, → N15, → N16N18letter-browserfullPageRoute (config)config—→ U12
Mermaid Diagram
Rendering diagram...
Slicing the Breadboard
With the full breadboard complete, slice it into vertical increments. Each slice demonstrates a mechanism working:
Slice Summary
#SliceMechanismAffordancesDemoV1Widget with real dataF1, F4, F6U2-U9, N3-N8, LD"Widget shows real data"V2Search worksF3U1, N1, N2"Type 'dharma', results filter"V3Infinite scrollF5U10, N11-N13"Scroll down, more load"V4URL stateF2U11, N9, N10, N14"Refresh preserves search"V5Compact mode—U12, N15-N18, LP"Shows 'See all' link"
Slice Diagram
Rendering diagram...