AI Design Import โ I/O Definition
This page is the official I/O contract for apps/design-import/. Any changes to fields, shapes, status values, or error categories constitute a contract change. Always update this page and test-case-design.md before writing code. The shared strict storage model in packages/models is authoritative for the persisted shape.
Reading order: Overview โ Input โ Processing contract โ Output โ Error handling โ Idempotency. The final Field reference section is for lookups.
Overview
flowchart LR
api[guinness-backend] -->|INSERT design status=0| RDB[(PostgreSQL)]
api -->|PUT image.png| S3
api -->|PUT schema.json| S3
api -->|SendMessage| Q[(SQS design-import)]
Q --> L[AI Design Import worker]
L -->|GET image.png| S3
L -->|GET schema.json| S3
L -->|UPSERT _id=design_id| Doc[(DocumentDB design)]
L -->|POST /v1/webhooks/ai-status| api
api -->|UPDATE design status=1 or 2| RDB
| Item | Value |
|---|---|
| Trigger | SQS record from the design-import queue |
| Input | 1 SQS message + 1 S3 image + 1 S3 Figma JSON |
| Output | 1 DocumentDB upsert + 1 Webhook POST |
| RDB writes | None โ the worker does not access PostgreSQL / MySQL (database isolation) |
| External calls | S3 GET (image + Figma JSON), current code-index read, Vision LLM via PydanticAI, provider-selected embeddings (1 variable-size batch), Webhook POST |
| AI framework | PydanticAI structured output; embeddings use the configured OpenAI/OpenRouter client |
Input
1. SQS Message (Trigger)
Lambda receives a standard AWS SQS event. The fields below are included as a JSON string in Records[*].body.
{
"Records": [{
"messageId": "0192a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b",
"body": "{\"organization_id\":1,\"project_id\":42,\"img_url\":\"s3://gnss-prod-inputs/designs/42_hDDA9B..._40002029:37033.png\",\"node_id\":\"40002029:37033\",\"file_id\":\"hDDA9BNori9OTXSClduXqR\",\"design_id\":\"42_hDDA9BNori9OTXSClduXqR_40002029:37033\",\"design_name\":\"Login Screen\",\"json_schema_url\":\"s3://gnss-prod-inputs/figma_json_schema/42_hDDA9BNori9OTXSClduXqR_40002029:37033.json\"}",
"attributes": { "ApproximateReceiveCount": "1" },
"eventSource": "aws:sqs"
}]
}
Body fields (parsed from Records[*].body, validated by the SqsBody Pydantic model):
| Field | Type | Required | Convention | Notes |
|---|---|---|---|---|
organization_id |
integer | โ | > 0 |
Tenant scope. Also copied to the DocumentDB document and echoed as organizationId in Webhook payloads |
project_id |
integer | โ | > 0 |
Project scope. Also copied to the DocumentDB document |
img_url |
string | โ | S3 URL (s3://... or HTTPS S3) |
Screenshot of the design |
node_id |
string | โ | Figma node id (typically <int>:<int>) |
Part of design_id |
file_id |
string | โ | Figma file id | Part of design_id |
design_id |
string | โ | Must match "{project_id}_{file_id}_{node_id}" โ enforced by a Pydantic model_validator |
Becomes the _id in DocumentDB |
design_name |
string | โ | Figma frame name | Stored as name and visual.metadata.name |
json_schema_url |
string | โ | S3 URL of the Figma node JSON export | Required matching evidence. Downloaded, parsed, and persisted as structural/node metadata |
figma_url |
string | null | โ | Canonical Figma URL | Derived from file_id/node_id when omitted |
Body unwrapping. The unwrap_sqs_body utility accepts the following 3 nested shapes (in priority order):
- Raw payload โ
bodyis the JSON described above. - SQS-in-SQS โ re-parses
body.Records[0].body. - API Gateway proxy โ
body.body(optionallyisBase64Encoded).
2. S3: Design Image
| Property | Value |
|---|---|
| Source | img_url from the SQS body |
| Size limit | 10 MB hard limit (MAX_IMAGE_BYTES in service.py) |
| MIME detection | Shared media-type inference from the canonical URL; fallback is image/png |
3. S3: Figma JSON
| Property | Value |
|---|---|
| Source | json_schema_url from the SQS body |
| SQS field | Required โ null or omitted is a validation error |
| Does the worker fetch it? | Yes โ downloads and parses it for every successful import |
| On failure | Sends failed Webhook and re-raises so SQS can retry |
Expected Figma JSON shape: {"nodes": {"<node_id>": {"document": <node>, ...}}}
4. RDB Row State (Read Elsewhere)
The worker does not read the PostgreSQL design row directly. Before sending the SQS message, the backend creates the row with status = 0. The worker only reports completion via Webhook; the backend manages the row's state machine.
Processing Contract
The worker executes the following 8 steps in order for each record. Messages that fail at any step proceed to the Error handling path.
-
Parse + validate: Validates the SQS body with
SqsBody. Verifies thatorganization_id > 0,project_id > 0, anddesign_id == f"{project_id}_{file_id}_{node_id}"; raisesValueErrorif they do not match. -
Download the design image: Fetches from S3. Raises an exception if the image exceeds 10 MB.
-
Download + parse Figma JSON with the shared deterministic evidence extractor to obtain:
- screen-level visible/generic terms and canonical structure text;
- bounded regions with node IDs, roles, query terms, bounds, composition counts, and typography evidence;
- concrete component instances with Figma component/component-set identities, properties, bounds, paths, and visible text.
If this step fails (missing keys, network error, etc.), the record fails. A successful design document always contains Figma-derived structural evidence.
-
Run the Vision Agent (PydanticAI,
run_sync): Obtains aDesignDescriptionfrom the image:Model used:class DesignDescription(BaseModel): layout: str # one of 11 archetypes component_types: list[str] # 3โ15 normalized snake_case names color_palette: list[str] # 2โ5 dominant hex colors (#RRGGBB) typography_style: str # one of 8 archetypes semantic_words: list[str] # 5โ10 domain keywords component_functions: list[str] # reusable component roles/functionsdesc_model(configured via environment variable, e.g.openai:gpt-5.4-nano). -
Require current code index and build embedding input texts:
- read the exact organization/project
code_indexand requirestatus=ready; - align index terms only when deterministic Figma evidence supports them;
-
build one semantic text per vision sample, one screen structure text, and one text per bounded region.
-
Generate semantic, structure, and region embeddings in a single batch call:
Semantic samples are averaged and normalized whenclient.embeddings.create( model=config.embedding_model, input=[*semantic_samples, structure_text, *region_texts], dimensions=config.embedding_dimensions, # default 512 )VISION_SAMPLES > 1; the remaining vectors map to the screen structure and each region in stable order. -
Replace a scoped
DesignDocumentin DocumentDB (_id == design_id). A stableprocessing.hashskips repeated paid work when all required vectors already exist. -
POST the success Webhook (to
webhook_base_url).
The document is written only after all required embeddings are available โ no partial documents exist.
Output
Output 1: DocumentDB design Document (upsert)
{
"_id": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"organization_id": 1,
"project_id": 42,
"name": "Login Screen",
"type": 0,
"based_on": 0,
"file_id": "hDDA9BNori9OTXSClduXqR",
"node_id": "40002029:37033",
"figma_url": "https://www.figma.com/design/hDDA9BNori9OTXSClduXqR?node-id=40002029-37033",
"json_schema_url": "s3://bucket/1/42/design/hDDA9BNori9OTXSClduXqR/40002029-37033.json",
"visual": {
"metadata": {
"name": "Login Screen",
"image_url": "s3://bucket/1/42/design/hDDA9BNori9OTXSClduXqR/40002029-37033.png",
"layout": "stacked_form",
"component_types": ["input_email", "input_password", "button_primary", "heading"],
"color_palette": ["#1A1A2E", "#FFFFFF", "#4A90E2"],
"typography_style": "modern_sans"
}
},
"semantics": {
"metadata": {"words": ["authentication", "button", "email", "form", "login", "password", "submit"]},
"vector_embedding": [/* 512 floats */]
},
"structure": {
"metadata": {
"visible_text": ["Email", "Password", "Login"],
"generic_terms": ["form", "button", "input"],
"text": "screen login structure with email password and submit regions",
"regions": [{
"metadata": {
"node_id": "40002029:37101", "role": "action", "query_terms": ["login", "button"],
"text": "login button", "bounds": {"x": 24, "y": 520, "width": 327, "height": 48},
"depth": 1, "direct_child_count": 1, "descendant_text_count": 1,
"descendant_instance_count": 1, "direct_child_type_counts": {"TEXT": 1},
"repeated_child_signatures": [], "font_weights": [600], "font_sizes": [16]
},
"vector_embedding": [/* 512 floats */]
}],
"component_instances": [{
"node_id": "40002029:37101", "name": "Button / Primary", "component_id": "comp_button_primary",
"component_key": "button-key", "component_name": "Button", "component_description": null,
"component_set_id": "button-set", "component_set_key": "button-set-key", "component_set_name": "Button",
"properties": {"State": "Default"}, "bounds": {"x": 24, "y": 520, "width": 327, "height": 48},
"depth": 1, "parent_node_id": "40002029:37033", "path": ["Login Screen", "Button / Primary"],
"visible_text": ["Login"], "summary": "Button / Primary State=Default text=Login"
}]
},
"vector_embedding": [/* 512 floats */]
},
"processing": {"hash": "<64-character SHA-256 fingerprint>"}
}
Field reference:
| Field | Type | Source |
|---|---|---|
_id |
string | SQS design_id |
organization_id |
integer | SQS organization_id |
project_id |
integer | SQS project_id |
name |
string | SQS design_name |
type |
integer enum | Default 0 (DesignType.SCREEN) |
based_on |
integer enum | Default 0 (DesignOrigin.FIGMA_IMPORT) |
file_id |
string | SQS file_id; Figma file key |
node_id |
string | SQS node_id; Figma node id |
json_schema_url |
string | SQS json_schema_url; downloaded and parsed |
figma_url |
string | SQS value or canonical URL derived from file/node IDs |
visual.metadata.layout |
string | Vision LLM output |
visual.metadata.component_types |
string[] | Vision LLM output (3โ15 items) |
visual.metadata.color_palette |
string[] | Vision LLM output, normalized #RRGGBB (2โ5 items) |
visual.metadata.typography_style |
string | Vision LLM output |
semantics.metadata.words |
string[] | Canonical vision terms plus evidence-aligned current-index terms |
semantics.vector_embedding |
float[512] | Averaged semantic vector when multiple vision samples are configured |
structure.metadata |
object | Screen text, bounded regions, and concrete Figma component occurrences |
structure.vector_embedding |
float[512] | Embedding of screen-level structure text |
structure.metadata.regions[].vector_embedding |
float[512] | Per-region structural query vector |
processing.hash |
string | Stable SHA-256 fingerprint used for unchanged-import reuse |
Write operation:
collection.replace_one(
{"_id": design_id, "organization_id": organization_id, "project_id": project_id},
doc.model_dump(by_alias=True, mode="json"),
upsert=True,
)
Indexes (created idempotently by packages/models/documentdb/design.py):
| Index name | Path | Dimensions | Similarity | m |
efConstruction |
|---|---|---|---|---|---|
design_scope_catalog |
organization_id, project_id, type, name |
โ | โ | โ | โ |
Design vectors are query vectors used against code/code-variation indexes; no design vector index is provisioned.
Output 2: Webhook โ success
POST {WEBHOOK_BASE_URL}
Content-Type: application/json
{
"type": "design-import",
"recordId": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"projectId": 42,
"organizationId": 1,
"status": "success",
"result": {
"keywords": ["login", "authentication", "form", "submit", "credentials"],
"textVectorDims": 512,
"vectorsProduced": ["semantic", "structure", "regions"]
}
}
| Field | Type | Notes |
|---|---|---|
type |
string | Always "design-import" |
recordId |
string | design_id from the SQS body |
projectId |
integer | From SQS project_id |
organizationId |
integer | From SQS organization_id |
status |
string | "success" |
result.keywords |
string[] | Same values as semantics.metadata.words |
result.textVectorDims |
integer | Number of dimensions for semantic, structure, and region vectors |
result.vectorsProduced |
string[] | Always ["semantic", "structure", "regions"] on success |
Output 3: Webhook โ failure
POST {WEBHOOK_BASE_URL}
Content-Type: application/json
{
"type": "design-import",
"recordId": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"projectId": 42,
"organizationId": 1,
"status": "failed"
}
The result block is absent on failure. A failure webhook is sent only after a valid body provides the record and scope IDs. Backend updates the design import status.
Error Handling
Partial batch failure support. The handler returns {"batchItemFailures": [...]}, so a single failure does not contaminate the rest of the batch. ReportBatchItemFailures must be enabled on the Lambda event source mapping. The worker emits a failure Webhook before marking an item as failed. SQS then retries only that failed item independently.
def lambda_handler(event, context):
failures = []
for record in event["Records"]:
try:
process_record(record, ...)
except Exception:
failures.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failures}
| Scenario | Behavior |
|---|---|
Invalid SQS event envelope (no Records) |
Raises ValueError. No Webhook. Entire invocation fails |
design_id does not match composite key |
Pydantic validation error. Failure Webhook. Added to batchItemFailures |
S3 GET for img_url fails |
Failure Webhook. Added to batchItemFailures |
| Image exceeds 10 MB | ValueError. Failure Webhook. Added to batchItemFailures |
Missing json_schema_url |
Validation error. Failure Webhook. Added to batchItemFailures |
S3 GET for json_schema_url fails |
Failure Webhook. Added to batchItemFailures |
| Figma JSON parse error | Failure Webhook. Added to batchItemFailures |
| Vision LLM exception | Failure Webhook. Added to batchItemFailures |
| Embedding API exception | Failure Webhook. Added to batchItemFailures |
| DocumentDB write exception | Failure Webhook. Added to batchItemFailures. Up to 2 retries for transient failures |
| Webhook POST itself fails | WARN log. Added to batchItemFailures so SQS retries |
| SQS retry count exceeded | Message is moved to DLQ (configured outside the worker) |
Idempotency
Three layers are applied in order.
- Fingerprint-aware scoped replacement: identical evidence reuses the valid stored vectors; changed evidence replaces the exact scoped document.
- Stateless worker: No writes to PostgreSQL and no local checkpoints.
- At-least-once Webhook: A failure Webhook followed by a success Webhook may arrive twice. The backend must be idempotent for
(type, recordId).
Locked Parameters
| Parameter | Value / Source | Notes |
|---|---|---|
| Embedding model | EMBEDDING_MODEL environment variable |
e.g. text-embedding-3-small |
| Embedding dimensions | EMBEDDING_DIMENSIONS environment variable (default 512) |
Must match HNSW index configuration |
| Vision model | DESC_MODEL environment variable |
e.g. openai:gpt-5.4-nano |
| Agent framework | PydanticAI | Not changeable via environment variable |
| Agent execution mode | run_sync (synchronous) |
Once per record |
| Embedding API calls per changed record | 1 variable-size batch | semantic sample(s) + structure + every region |
| Image size limit | 10 MB | Hard-coded as MAX_IMAGE_BYTES in service.py |
| Vision timeout | 60 seconds | VISION_TIMEOUT_SECONDS in service.py |
| Vision samples | VISION_SAMPLES, default 1 |
When greater than one, semantic sample vectors are averaged and normalized |
Logging
Structured fields bound per record: app=ai-design-import, message_id, prompt_version, design_id, organization_id, project_id.
| Event | Level | Timing |
|---|---|---|
design_import.received |
INFO | First line of process_record |
design_import.parsed |
INFO | After Pydantic validation |
design_import.s3.image.downloaded |
INFO | After image GET (size_bytes, media_type) |
design_import.figma_evidence.extracted |
INFO | After Figma JSON parse (visible text, region, component-instance counts) |
design_import.vision.completed |
INFO | After Vision LLM (sample/component/semantic-word counts and layout) |
design_import.embeddings.completed |
INFO | After embedding call (dimensions and sample count) |
design_import.documentdb.upserted |
INFO | After upsert (matched, modified, upserted_id) |
design_import.webhook.sent |
INFO | After Webhook POST (http_status) |
design_import.failed |
ERROR | Terminal failure inside process_record |
design_import.webhook.failed |
WARN | Webhook POST itself failed |
Field Reference (Lookup Table)
| Field | SQS in | Doc out | Webhook out | Notes |
|---|---|---|---|---|
design_id |
โ | _id |
recordId |
Composite id (enforced validation) |
organization_id |
โ | โ | organizationId |
tenant scope |
project_id |
โ | โ | projectId |
project scope |
design_name |
โ | name, visual.metadata.name |
โ | |
type |
โ | โ (default 0) |
โ | screen |
based_on |
โ | โ (default 0) |
โ | Figma import |
img_url |
โ | visual.metadata.image_url |
โ | |
json_schema_url |
โ | โ | โ | Required Figma node JSON; downloaded and parsed |
node_id, file_id |
โ | โ | โ | Components of design_id; stored for downstream evidence |
figma_url |
optional | โ | โ | Supplied or canonically derived |
visual.metadata.* |
โ | โ | โ | name, image, layout, component types, palette, typography |
semantics.metadata.words |
โ | โ | result.keywords |
|
semantics.vector_embedding |
โ | โ | โ | 512 floats |
structure.metadata.text |
โ | โ | โ | screen-level structure text |
structure.metadata.regions |
โ | โ | โ | bounded region metadata + 512-vector each |
structure.metadata.component_instances |
โ | โ | โ | concrete Figma occurrence evidence |
structure.vector_embedding |
โ | โ | โ | screen structure vector, 512 floats |
processing.hash |
โ | โ | โ | unchanged-import fingerprint |
status |
โ | โ | "success" \| "failed" |
|
result.textVectorDims |
โ | โ | โ | |
result.vectorsProduced |
โ | โ | โ |
Related Links
- Overview โ Processing flow diagram and module breakdown.
- Test cases โ Test matrix that enforces this contract.
packages/models/documentdb/design.pyโ scoped catalog index definition.packages/agentic/vision.pyโDesignDescriptionstructured output schema.packages/des2code-coreโ deterministic Figma JSON evidence extraction.