AI WF2Des — System Workflows
The other pages specify the pieces — the Overview for the worker and its
generation and internal event paths, the Assembly Architecture for the engine,
the I/O Definition and the
Internal API for the field-level contracts. This page
shows the pieces moving: every runtime flow, end to end, across the backend, the
apps/wf2des worker, wf2des-api, and the Figma plugin.
The Landscape
flowchart LR
subgraph SURFACES["Surfaces"]
PL["Plugin (in Figma)"]
AC["API callers"]
MA["MCP agents"]
end
subgraph BE["Backend — product control plane"]
API["wf2des API<br/>trigger · registration · status · list ·<br/>confirm · cancel ·<br/>placement / feedback flags"]
PG[("PostgreSQL wf2des row<br/>status · phase · attempt ·<br/>open-generation partial unique")]
DREG[("platform design table<br/>type=component — the SHARED registry")]
SQSG[("generation SQS queue<br/>(backend-owned)")]
WH["POST /v1/webhooks/ai-status<br/>(X-API-Key) + registry apply"]
end
subgraph AI["AI side — guinness-ai-v2"]
WK["apps/wf2des worker<br/>generation + internal events · never writes PG"]
SQSE[("wf2des-events intake queue<br/>(AI-owned; backend send-only)")]
EB["EventBridge schedule<br/>(component resync)"]
DAPI["wf2des-api<br/>(internal-api route group)"]
LLM["LLM<br/>model + prompt version pinned"]
end
subgraph ST["Stores"]
DD[("DocumentDB guinness_v2<br/>wireframe · design_rule · design_component ·<br/>project_figma_file · design_generation_result ·<br/>design_resolution")]
S3[("S3 — snapshots · artifacts ·<br/>result manifests")]
end
FIG["Figma file"]
PL -->|"trigger · registration · poll ·<br/>confirm · cancel · flags · discovery"| API
AC --> API
MA --> API
API -->|"row FIRST — status '0', phase parse"| PG
API --> SQSG
API -->|"rule / plugin events"| SQSE
SQSG --> WK
SQSE --> WK
EB --> WK
WK <--> LLM
WK --> DD
WK --> S3
WK -->|"completion webhook {job_id, attempt, nonce,<br/>status, error?, result_manifest_url?,<br/>manifest_schema_version}"| WH
WK -->|"component_sweep manifest<br/>(discovered type=component)"| WH
WH -->|"attempt-guarded flip: status/phase<br/>+ result refs + flag_count"| PG
WH -->|"design type=component upserts"| DREG
PL <-->|"data plane: file reg · preview ·<br/>spec · details"| DAPI
MA -.->|"spec reads"| DAPI
DAPI <--> DD
DAPI <--> S3
WK -.->|"REST read (PAT): component walk ·<br/>rule-board render · memo fallback"| FIG
API -.->|"trigger-time snapshot (service-account PAT):<br/>frame + board section"| FIG
PL ==>|"the ONLY writes into Figma"| FIG
Three laws shape every flow on this page. One — only a Figma session writes into Figma:
Figma's REST surface has no design-writing endpoints, so every pixel that appears in a file is
created by the plugin's main thread inside a designer's editor session. Two — workers never
write PostgreSQL: every PG write is backend-side — the trigger and frame-registration
endpoints, the confirm / cancel / placement / feedback endpoints, the ai-status
webhook handler at generation completion, plus the component-registry upserts, where the backend writes the platform design
table (type=component) from component_sweep's manifest. Three — register → process →
commit: the backend registers the run (the wf2des row plus immutable S3 inputs), the
worker processes it (DocumentDB context + S3 artifacts and manifests), and the commit is
backend-side (the generation webhook flips the row; the backend applies component_sweep's
manifest to the registry).
Status vocabulary used throughout: status '0' processing · '1' completed · '2' failed ·
'3' rejected · '4' cancelled; phase lives inside status '0' — '0' parse · '1'
awaiting_confirm · '2' assemble — and is cleared once the row reaches a terminal status.
Project Onboarding — Once per Project
Onboarding is throughput work, not latency work: it happens once per project, and everything generation later pins — files, components, rules, wireframes — enters here.
flowchart TD
A1["1 · Project exists on the platform;<br/>the service account has view access"]
A2["2 · Register the Figma files —<br/>POST /internal/projects/{project_id}/figma-files via wf2des-api<br/>(role: working | library) → project_figma_file docs;<br/>also the plugin's org/project + file resolution"]
A3["3 · COMPONENTS — component_sweep populates the SHARED registry:<br/>EventBridge schedule / plugin resync event →<br/>Figma REST full-file walk (/components lists published only) →<br/>design_component context docs (variants · text slots · size)<br/>+ the sweep manifest → backend UPSERTS platform design rows type=component"]
A4["4 · RULES — designer selects guideline boards in the plugin →<br/>rule event on wf2des-events → rule_process renders each board (PAT)<br/>+ vision-extracts per board + deterministic merge →<br/>versioned IMMUTABLE design_rule doc, worker-written directly<br/>(webhook-free; keyed (design_rule_id, content_hash of the merged RuleSet))"]
A5["5 · WF REGISTRATION — designers register selected frames<br/>in-Figma via the plugin → backend snapshot +<br/>registration events on wf2des-events → wf_parse →<br/>wireframe cache docs"]
A6["6 · INGEST ISSUES LOG (staging/issues.json) —<br/>memo-marker unknowns · screen-ID grammar violations ·<br/>non-auto-layout roots — reviewed BEFORE generation<br/>runs against the file"]
A1 --> A2 --> A3 --> A4 --> A5 --> A6
Two properties of this flow matter downstream. First, all three ingest runs are idempotent: each output document is its own record, so a replayed event converges rather than duplicates. Second, the WF snapshot captured at registration is a cache, not a commitment — generation re-snapshots the frame at trigger time, so a stale registration never feeds a run.
Generation via the Plugin — The Primary Flow
The interactive flow: a designer selects a wireframe frame, the backend registers and snapshots it, the worker parses, the designer confirms the parse, the worker assembles, and the plugin materializes the result as native Figma.
sequenceDiagram
actor D as Designer
participant P as Plugin
participant A as Backend (API + PG)
participant Q as Generation SQS
participant W as wf2des worker
participant X as wf2des-api
D->>P: select WF frame + screen ID (prefilled)
P->>A: POST trigger (figma_file_key, wf_node_id, screen_id, auto_confirm, prompt?, placement_target?)
A->>A: INSERT wf2des row FIRST — status '0', phase parse<br/>(partial-unique dedupe hit → 409 + the existing wf2des_id)
A-->>P: 201 + wf2des_id (immediate)
Note over A: SNAPSHOT AT TRIGGER — between row creation and enqueue,<br/>Figma REST (the wf2design service-account PAT) captures the frame<br/>+ its enclosing board section → S3 + wf_content_hash;<br/>unregistered frames auto-register
A->>Q: parse message {wf2des_id, attempt, nonce, snapshot scope + hash, input URLs}
Q->>W: PARSE (one-shot)
Note over W: deterministic WFNode extraction + LLM roles/intent →<br/>result-doc parse block + immutable parse.json<br/>(DocDB commits CAS-fenced on (wf2des_id, attempt))
W->>A: webhook parse_done {job_id, attempt, nonce}
A->>A: wf2des row → phase awaiting_confirm
P->>A: poll GET (backoff) — status '0', phase awaiting_confirm
P->>X: GET /internal/wf2des/{wf2des_id}/parse
X-->>P: parse block + presigned parse.json
P-->>D: PARSE PREVIEW — structure + memo influences (resolved memos excluded) + snapshot age/hash
D->>P: confirm or reject
alt reject
P->>X: POST /internal/wf2des/{wf2des_id}/reject {reason_code, note} → parse.rejected (detail FIRST)
P->>A: POST confirm {attempt, reject: true} → status '3' rejected (flag LAST)
else confirm
P->>A: POST confirm {attempt, parse_artifact_hash}
A->>A: CAS on phase awaiting_confirm + attempt → phase assemble
A->>Q: assemble message {wf2des_id, attempt, fresh nonce, confirm decision}
Q->>W: ASSEMBLE (one-shot — pins rehydrated from the result doc)
Note over W: registry hygiene + deterministic role/kind gate →<br/>design_resolution ledger consult → section-parallel K-sample<br/>self-consistency voting selection → ledger write-back → deterministic stitch →<br/>rules validator → computed confidence
W->>W: S3 result artifact + result manifest
W->>A: webhook succeeded {job_id, attempt, nonce, result_manifest_url}
A->>A: handler — deduped on (job_id, attempt, nonce), ONE PG txn:<br/>status '1' + result refs + flag_count, phase cleared
P->>A: poll GET → completed (+ top-level flag_count)
P->>X: GET /internal/wf2des/{wf2des_id}/result
X-->>P: self-contained spec (no follow-up lookups)
Note over P: MATERIALIZE — chunked build ·<br/>pluginData {jobId, specVersion} stamped
P-->>D: new tagged frame, flags visualized
P->>X: POST /internal/wf2des/{wf2des_id}/placement (detail FIRST)
P->>A: POST placement → materialized_at (flag LAST)
D->>P: adopt / fix
P->>X: POST /internal/wf2des/{wf2des_id}/feedback (detail FIRST)
P->>A: POST feedback → feedback_status (flag LAST)
end
The load-bearing details:
- Row first, then enqueue. The
wf2desrow exists before the job does, so a missing-row read inside the worker is a real error, never an eventual-consistency window. The open-generation partial unique —(figma_file_key, wf_node_id) WHERE status='0'— dedupes concurrent triggers: the second caller gets 409 + the existingwf2des_idand attaches to the running generation. - The trigger sends identifiers, not trees. The wireframe content never travels from the
plugin; the backend captures the snapshot server-side at trigger time, scoped to the frame
plus its enclosing board section, and the SQS message carries the scope +
wf_content_hash. - The confirm checkpoint is a CAS. Confirm carries
{attempt, parse_artifact_hash}and is compare-and-swapped against the row'sphase awaiting_confirm+attempt— a stale confirm gets 409, a repeat gets a 200 echo. - Detail first, flag last — every plugin write pair lands its content detail in
wf2des-api(DocumentDB) before flipping the corresponding flag on the backend row:parse.rejectedbeforestatus '3', the placement detail beforematerialized_at, the feedback detail beforefeedback_status. - One completion writer. The
ai-statushandler is the sole completion-time PG writer: deduped on(job_id, attempt, nonce)—job_idis thewf2desrow id — attempt-guarded, one PostgreSQL transaction, applying the manifest referenced byresult_manifest_url.
Soft latency targets (p50, measured — not gates): trigger→parse preview < 60s ·
confirm→spec < 2min · spec→materialized < 30s. Every phase is timestamped on the
result doc's timings block.
Generation via API / MCP — Differences Only
An API caller or an MCP agent runs the same wire as the plugin, with three differences:
auto_confirm= one invocation. The same backendPOSTwithauto_confirm=trueruns parse + assemble back-to-back inside a single worker invocation — noawaiting_confirmpark, no confirm round-trip, and only the terminal webhook fires. The parse record (memo influences, roles) is still written, for review at materialize time.- Status from the backend, content from
wf2des-api. The caller polls the backendGETto completion (+ top-levelflag_count); the spec is read fromGET /internal/wf2des/{wf2des_id}/result— never from a Figma frame, because no frame exists yet (the write boundary). - MCP agents ride the same two planes. The backend MCP server fronts the trigger/status
tools — following the platform's
trigger-*/get-*tool naming; the final tool names land with the MCP wrapper — withauto_confirmforced: no human sits at the parse checkpoint. Content reads go throughinternal-apiwith the read-scoped service token. An MCP agent triggers and reads; it never writes into Figma (a later, prototyping-only path can drive an open Figma desktop session — which is "inside Figma", on channel 4's side of the wall).
The generated design becomes native Figma later, through deferred materialization — next.
Deferred Materialization
After the native frame is built, the plugin captures its actual PNG, node/bounds manifest, and
materializer report and submits a hash-fenced render_review event. A passing review retains the
frame. A first needs_changes result may create one adjacent candidate while preserving the
original; a second render verifies that candidate. Only a pass promotes it in place. Failure,
stale identity, or unverifiable evidence discards the candidate and retains the original. The
second review is verification-only, so there is no recursive correction loop.
The plugin session and the job are decoupled: closing Figma mid-job loses nothing, and an
API/MCP-triggered generation simply waits, completed, with materialized_at unset. On every
open, the plugin runs a discovery handshake against the backend — readiness is
client-facing status, and that lives in PostgreSQL:
- The general list endpoint (
GET …/wf2des, filtered bystatus/phase/screenId/feedbackStatus/materialized/createdFrom/createdTo) returns two kinds of hits for the open file: parked previews (phase=awaiting_confirm— the designer resumes the confirm/reject decision, or ends the job viaPOST …/wf2des/{id}/cancel) and pending builds (completed rows withmaterialized_atunset, the storedplacement_targetincluded). The same endpoint serves project history. - The duplicate-build guard runs before any pending build is offered: the plugin checks
for an existing placement detail via
wf2des-apiand scans the file forpluginData {jobId}. A session that crashed between the placement detail and thematerialized_atflag therefore never produces a second frame. - Auto-confirm jobs get their skipped review here. For an API/MCP-triggered job, the plugin first fetches the parse record and surfaces the memo influences and flags — the checkpoint no human sat at — before offering the build.
- The spec is pulled from
wf2des-api, built at the storedplacement_target, and closed out with the normal placement detail + flag pair. The reviewing designer's adopt/fix feedback attaches at this materialize-and-review moment — exactly like the interactive flow's last step.
This handshake is how an API caller's design reaches native Figma: any designer who later opens the file is offered the pending build.
The Four Figma Channels
Everything between Figma and the system travels over exactly four channels. Figma never calls in — there are no Figma webhooks today; all movement is initiated by the plugin, by the backend (the trigger-time snapshot), by the worker, or by an operator.
| # | Channel | Direction | Initiator | Auth | Carries |
|---|---|---|---|---|---|
| 1 | Plugin → backend API (control plane) | out of Figma | plugin UI | the platform's auth (login in the plugin UI) | trigger (identifiers only), frame registration + resync, status polls (+ phase), confirm/reject, cancel, placement + feedback flags, discovery via the general list endpoint |
| 2 | Plugin ↔ wf2des-api (data plane — the wf2des routes in internal-api) |
both | plugin UI | session-issued token {org_id, project_id, exp} |
file registration (project_figma_file role/config), parse preview, the spec (self-contained), placement + feedback details |
| 3 | Server side → Figma REST (read) | into the system | worker · the backend at trigger | the wf2design service-account PAT (wf2design's own secret store — never the platform figma_token table) |
component full-file walk, memo-fallback node reads, WF-registration snapshots, the trigger-time WF snapshot (frame + enclosing board section) |
| 4 | Plugin main thread ↔ the open file (Figma Plugin API) | inside Figma | plugin main thread | none — the designer's own session | read the selected WF frame; write the generated frame (instances, overrides, pluginData) |
Channel 4 is the only write path into Figma anywhere in the system; channel 3 is the only
server-side read, and it is read-only by Figma's own API design. The 1-vs-2 split mirrors the
storage split: status truth lives in PostgreSQL and is served by the backend; content lives in
DocumentDB + S3 and is served by wf2des-api (see the
Internal API contract). The plugin stitches the two
with the wf2des_id it receives at trigger time.
Inside the plugin, the two halves have disjoint powers, so every flow is a relay:
flowchart LR
subgraph FIGMA["Figma editor session"]
FILE[("open file<br/>WF frames · components ·<br/>generated frames")]
MAIN["plugin MAIN THREAD<br/>(sandbox — figma.* only, no network)"]
UI["plugin UI IFRAME<br/>(network only, no figma.*)"]
FILE <-->|"read selection / build nodes"| MAIN
MAIN <-->|postMessage| UI
end
UI -->|"1 · control: trigger · frame reg + resync ·<br/>poll · confirm · cancel · flags · discovery"| BE["Backend API<br/>(PostgreSQL)"]
UI <-->|"2 · data: file reg · preview ·<br/>spec · details"| XAPI["wf2des-api<br/>(DocumentDB + S3)"]
W["wf2des worker"] -->|"3 · REST read (PAT):<br/>component walk · memo fallback ·<br/>WF-registration snapshots"| FC["Figma cloud<br/>(REST API)"]
BE -->|"3 · REST read at trigger<br/>(service-account PAT)"| FC
The materialization mechanics on channel 4:
- The same-file constraint. Components are instanced directly by
node_id(Figma's/componentslists published components only, so local components have no publish key) — the placement file must contain the source components, and the materializer refuses with a clear error otherwise. - Overrides by layer name/path, never ordinally — matching the spec's
layer_pathaddressing, so a reordered layer list never mis-targets an override. - Always a new tagged frame. The build never overwrites; the output frame is stamped
pluginData {jobId, specVersion}. - JP-font preflight. The plugin lists the spec's fonts, probes availability in the editor, and warns before the build starts.
- Chunked builds. Large specs build in chunks so the editor stays responsive.
- Identifiers, not trees. The WF content never travels from the plugin — the backend snapshots the frame server-side at trigger time, so channel 1 stays thin and the job's input is content-addressed in S3.
Feedback — Bookkeeping Only
The designer marks the materialized result adopted or fixed. The plugin writes the
detail to wf2des-api first — POST /internal/wf2des/{wf2des_id}/feedback with
{status, changed_nodes[], diff_url, at, by}, where status is adopted | fixed,
changed_nodes are spec layer_path values, and oversized inline diffs spill to S3 — then
flips feedback_status on the wf2des row via the backend feedback endpoint (flag last).
And that is the end of the flow: no event is emitted, no worker leg runs, and there is no
automated learning from the diffs.
The stored adopt/fix diffs are review material for the designer's own rule and component revisions, and those revisions re-enter the system through their normal channels — a rule edit travels the rules-update flow below; a component edit is picked up by the next registry resync.
Rules Update
A designer updates the project's guideline boards in Figma and re-registers them: the
designer selects the guideline board frames in the plugin → the backend emits a rule-upload
event ({design_rule_id, figma_file_key, board_node_ids[]}) onto wf2des-events →
rule_process renders each selected board over Figma REST (service-account PAT; a missing
board or a failed render is a hard error), runs one vision extraction per board (LLM
fence call 4), merges the fragments deterministically, and writes a new immutable,
versioned design_rule document directly to DocumentDB — one doc per
(design_rule_id, content_hash of the MERGED RuleSet), with a version ordinal, the board
image refs, extraction provenance, and draft_source="llm_extracted". The extraction lands
pending designer review: the designer inspects the extracted rules and re-registers after
guideline fixes — identical merged rules converge on the existing revision, changed rules
mint the next version. The run is webhook-free and PG-free; the design_rule collection
is the rule's revision history.
Versioning is what keeps this safe mid-flight: running generations keep their pinned
(design_rule_id, content_hash) — a rule edit never changes a run already in progress —
while new generations pin the latest revision at trigger time.
Registry Resync
A resync fires from either the AI-owned EventBridge schedule or a plugin resync event
on wf2des-events → component_sweep walks the project's registered files over Figma REST,
hash-diffing against stored content hashes so only changed components are re-snapshot. Two
outputs follow:
- The DocDB context, written directly:
design_componentdocs (variant properties, text slots, default size), keyed to the platformdesignrow id, plus field-level stamps onproject_figma_file—components_synced_atlast,sweep_erroron failure. - The discovered-component manifest, applied backend-side: the backend upserts platform
designrows (type=component) — the shared component registry, the same catalogdes2codeconsumes — which owns each component's existence, name, status, and removal.
A single-flight guard — the sweep_marker on the project_figma_file doc — keeps two
concurrent sweeps from racing the registry; stale references surface through output-doc
freshness (components_synced_at) and the next sweep's re-convergence.
Note the asymmetry with the other internal runs: wf_parse and rule_process are entirely
self-contained (the output doc is the record), while component_sweep additionally hands the
backend its sweep manifest for the registry upsert — a separate path from the generation-only
ai-status webhook.
When Things Fail
| Failure | What happens | Who sees what |
|---|---|---|
| Worker crash mid-job | One-shot Lambda dies → SQS redelivery (partial-batch); retries exhausted or row stuck '0' → no recovery path today: the row stays non-terminal and keeps blocking new triggers for that wireframe |
ops / plugin Retry button |
| Generation fails | …-failed.json artifact + failed webhook {job_id, attempt, nonce, status: failed, error, result_manifest_url → …-failed-manifest.json} → handler records error on the row, flips status '2', clears phase |
designer sees the reason + Retry |
| Provider credits exhausted, credentials/access invalid, or configured model unavailable | terminal failed artifact + manifest + safe client-visible failed webhook; consume because redelivery cannot repair configuration/account state | designer sees a concrete provider error |
| Ordinary provider 429 rate limit or 5xx | no terminal surface; throw for SQS redelivery | temporary retry state |
| Parse looks wrong | designer rejects the preview — detail {reason_code: wrong_roles \| wrong_memos \| wrong_sections \| other, note} to wf2des-api first (→ parse.rejected), then the backend flips status '3' rejected (detail first, flag last); a retry after reject skips the parse cache |
clean stop, no spec; the reason feeds parse-config curation |
| Awaiting-confirm forgotten | a backend-side sweep over wf2des rows enforces the timeout (e.g. 72h) → fails closed, reason recorded |
designer notified on the next poll |
| Preview parked (designer left mid-decision) | the job holds at phase=awaiting_confirm — nothing runs, nothing is lost; nothing expires it |
any later session finds it via the general list endpoint and resumes confirm/reject — or ends it via POST …/wf2des/{id}/cancel |
| Webhook undelivered | the worker's send raises on failure → SQS redelivers and the send retries; the terminal S3 manifest stays the durable completion record. If the retries are exhausted the manifest is never applied — the stuck-generation sweep that would scan and replay it is NOT implemented, so the row stays non-terminal. The webhook is generation-only — component_sweep needs no delivery: its context and manifest re-converge / re-apply on the next sweep |
self-heals only while redelivery lasts; after the DLQ, manual |
| Duplicate trigger (same WF already in flight) | the open-generation partial unique (figma_file_key, wf_node_id) WHERE status='0' dedupes → 409 + the existing wf2des_id |
both callers poll the same row |
| SQS send fails after row creation | the trigger returns 500 and best-effort fails the row to '2' so the node frees up. If that fail-out also fails there is no backstop — the stuck-'0' sweep is NOT implemented, and the row blocks every future trigger for that wireframe until cleared by hand |
immediate error; manual if the fail-out did not land |
Internal run fails (wf_parse · rule_process · component_sweep) |
SQS redelivery on wf2des-events / EventBridge retry + idempotent content-addressed / LWW / upsert writes that converge; stuck-run visibility = output-doc freshness + the SQS dead-letter queue — the backend never sees internal runs |
ops (output freshness + the DLQ) |
| Materializer can't resolve or safely apply content | name_fallback, ordinal_fallback, prop_rejected, unmatched, or preserved is recorded in materializer_report with its actual detail |
visible actionable diagnostic |
| A node can't build at all | build_error is visible and never silently dropped; font_fallback is separate when a substitute font rendered |
placeholder or degraded-font warning |
| Build fails midway (e.g. a component deleted between trigger and build) | pinned inputs keep the spec valid; unresolvable nodes flag (build_error badges) instead of aborting the build |
a built frame with flagged nodes — never a half-done silent failure |
| Session crash between placement detail and flag | the detail exists in wf2des-api but materialized_at never flipped |
the duplicate-build guard: the next session checks the placement detail via wf2des-api and scans in-file for pluginData {jobId} before rebuilding — never a second frame |
| Pinned input missing / hash mismatch on rehydrate | the job fails closed — never a silent fallback to older rules or components | error + Retry after the fix |
| Fonts unavailable in the editor | the JP-font preflight lists the spec's fonts, probes availability, and warns before the build | font warning before anything is built |
| Components not in the placement file | components are instanced by node_id, so the placement file must contain the sources (the same-file constraint) |
the materializer refuses with a clear error; build in the file that holds the components |
wf2des-api unreachable, backend up |
data-plane outage; content reads retry with backoff — no state is lost, each store stays authoritative for its own half | status remains visible via the backend poll |
The recovery spine underneath the table: the terminal S3 manifest is the durable record
and the webhook is only the fast path; every row effect is idempotent under the
(job_id, attempt, nonce) dedupe; and completed generations are never reprocessed — the
a terminal row is final and re-running a wireframe creates a NEW row, because a re-run would
overwrite committed human-event data with a different nondeterministic spec.
Official field-level contract: I/O Definition.