# Integration Builder

> Plan, map, build, and verify fastn integrations and workflows through the fastn Workflow MCP, in four phases — PLAN (discover connectors and entities, recommend a plan), MAP (probe fields, propose mappings and filters, approve, save the config), BUILD (workflows that read that config at runtime so edits need no code, test live, bind triggers, end with a widget), VERIFY (fire every trigger, correlate to a real execution, self-heal, close with a human-readable Markdown verification report). Any source→target data movement — syncs, aggregations, reshapes — plans and maps first. A single-system automation carrying user-editable business rules (a filter, a threshold, a status) still maps those into an approved config; only a rule-free one builds directly. Routes to connector-builder when a connector, action, event, or connection is missing. Use to sync, integrate, connect, or migrate data between systems, or to build or modify any fastn workflow, automate on an app event, schedule a sync, or expose a webhook.

- Skill: `fastn-ai/integration-builder` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add fastn-ai/integration-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fastn-ai/integration-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: fastn-ai (https://skillmd.com/u/fastn-ai)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/fastn-ai/integration-builder

---


<!-- fastn skill: integration_builder v16 -->

> Step 0 - version sync (do this FIRST, every run): this file is tagged `<!-- fastn skill: integration_builder v16 -->`. Call `skill {"slugs": ["integration_builder"]}` on your fastn gateway and compare. If the published version differs, reinstall from the fresh `downloadUrl` before doing anything else.

# integration-builder

## How to run
## About fastn (product context)

fastn is the **embedded integration layer for SaaS — the "Zapier for SaaS."** A SaaS company embeds fastn once, and its own customers configure their integrations with no engineering and no per-account code. fastn automatically handles auth, data mapping, business logic, retries, idempotency, batching, throttling, rate limits, and observability. For SaaS agents, an MCP gateway gives the same unified, governed, audited tool access inside customers' systems of record (Salesforce, NetSuite, ServiceNow). Vision: become the default embedded integration platform for SaaS.

**The build pair:** use **integration-builder** (this skill) to plan, map, and build the running integration — one continuous flow — and **connector-builder** to build or debug a connector. If a needed connector or action is missing, that is connector-builder territory.

Workflows are JavaScript modules running in an isolated V8 sandbox against real third-party APIs — **treat every live call as writing to a customer's production system.**

---



A data-movement integration runs through four phases, in order. Each phase has a dedicated reference file with the full procedure — **open it when you enter that phase.** This SKILL.md is the orchestrator: it tells you which path to take, the non-negotiables, and which file to read next.

```
PLAN ──────────────▶ MAP ───────────────▶ BUILD ──────────────▶ VERIFY
discover, analyze,   probe real fields,   build workflows that   fire every trigger, read back
ask business Qs,     propose mappings,    read the saved config  widget/config, correlate runs,
decide tech design   approve → configId    at runtime, test,      self-heal, Markdown REPORT
references/plan.md    references/mapping.md trigger, widget        workflow-verifier skill
                                          references/build.md    (references/verify-matrix.md)
                                          + dynamic-config.md + sandbox.md
                                          + workflow-patterns.md
```

## ROUTER — which path?

Two questions decide the path, not one. **Q1: does this build MOVE data from a source into a target?** **Q2: does it carry BUSINESS RULES the user will want to change later** — a filter or category set, a threshold, a routing target, a status/warehouse/owner it selects? Q1 decides whether you need field mappings. **Q2 decides whether you need a saved config, and it is independent of how many connectors are involved.**

- **Data-movement integration** — any source → target flow (2 or 3 connectors; 1:1 record sync, 3-connector enrich-and-sync, OR aggregation/reshape like thousands of stock rows → a few "in stock / low / out" fields) → run **all three phases: PLAN → MAP → BUILD.**
- **Single-system automation that carries business rules** — one connector, no data crossing systems, but the logic names values a human will want to edit: *"when an order is placed in Brightpearl with a product in categories X or Y, switch it to the Main warehouse."* The categories are a **filter** (`in` / `not_in`) and the warehouse is a **selected target value** — both are exactly what a config holds and what the review page renders. → **run a SCOPED MAP** (probe the entity, propose the conditions + the selected value, `propose_configuration` → `reviewUrl` → `configId`), then the GATE, then BUILD reading that `configId`. Skip PLAN's cross-system discovery — there is no second system to discover — but do NOT skip the config. Baking `["X","Y"]` and `"Main"` into workflow code makes changing a category an engineering ticket, which is precisely what the DYNAMIC CONFIG principle exists to prevent.
- **Single-system automation with no editable rules** — an event handler, scheduled job, inbound webhook endpoint, callable API, or one-connector batch job that does NOT move data into another system, and whose logic names nothing a user would reasonably want to retune (a fixed notification, a parameterless job, a passthrough webhook). → **run the test-case GATE, then BUILD** (`references/build.md`), no config. The GATE is never optional here.
- **Unified API ask** — "one canonical endpoint across N providers", "normalize connectors behind one surface", reading/writing an entity that the platform unified catalog already covers (`list_unified_categories`), or attaching a unified entity to a widget → follow the **unified-api** skill. In workflow code prefer `fastn.unified.<category>.<entity>.*` over per-provider `fastn.connector.*` whenever a catalog entity covers the data — mappings stay editable in the catalog without code changes.

Both single-system branches cover the same shapes — event handler, scheduled job, webhook endpoint, callable API, one-connector batch job. Q2 is what separates them: whether the logic carries a value a human will want to change.

**"Single-system" is a CONFIRMED fact, never your inference:** take either single-system branch only when the user named the system(s) or exactly one connector is connected — otherwise (several connected, request phrased in generic nouns like "an order with a product in category X → switch warehouse") ASK where the trigger fires / the deciding value lives / the change is applied, and never announce the path before that answer. Unsure or mixed → take the full three-phase path.

**Do not use connector count as the config test.** "Only one connector, so no mapping needed" is the wrong reflex and the most common way a build ends up with hardcoded business rules. The test is Q2: *if the user changed their mind about a value in this automation tomorrow, would they have to come back to an engineer?* If yes, that value belongs in the config.

Either way: the PREREQUISITES gate below applies throughout, and **the test-case GATE runs for EVERY build**. The GATE is **per-workflow, not per-config** — a single-connector automation with no `configId` still derives its cases and gets them user-approved before BUILD, exactly like a sync does. "No MAP / no config" is NOT license to author-and-self-validate test cases; the only difference is WHERE the cases come from (the workflow's own logic instead of a saved config). Never treat the GATE as a three-phase-only step.

## SECOND AXIS — single-tenant (Path A) vs multi-tenant (Path B)

Orthogonal to "does it move data?" is a second question that decides whether you touch the connector **manifest**: **whose accounts does this flow run against — your own (personal / internal), or your customers' (a product they use to connect THEIR accounts)?** This is a **business question, not a silent technical call** — so **ALWAYS ASK, never infer**: analyze the use case, pick the likely path, and put it to the user as an **explicit question** with your recommendation pre-selected as the default (for data-movement flows it rides along in the PLAN business-question batch — `references/plan.md` Step 4; for PLAN-skipping single-system builds, ask it before the manifest/widget step). The user one-clicks the recommended default or flips it — but the question is always shown, even when the framing looks obvious. Full model in `references/multi-tenancy.md`. **Either way the build ends with a widget (BUILD step 8) — the widget is always created.** The only thing this axis changes is the manifest scope (`set_connector_scope`).

- **Path A — single-tenant** (the developer's own data, internal automation): *"Sync MY X to MY Y."* `probe → create_workflow → test_workflow → bind trigger → create_widget`. The manifest stays all-`SAAS` — you **don't call `set_connector_scope`**. **This is the default for everyday automations.**
- **Path B — multi-tenant** (a product shipped to the partner's customers): *"Let MY CUSTOMERS sync THEIR X to THEIR Y" / "build a widget" / "embed."* `probe → create_workflow → set_connector_scope (MULTI_TENANT) → self-install test → propose_configuration → configId (the template) → create_widget(configId) → publish`. The extra step here is **manifest tuning** — flipping per-customer connectors to `MULTI_TENANT`.

So the widget closes **both** paths (BUILD step 8): bound to the saved config when MAP produced one, bound to the workflow's connectors when it did not. The **`set_connector_scope` / `MULTI_TENANT` manifest tuning is the Path-B-only part** — not the widget. **Your recommended default comes from the request's framing:** "my data / internal / sync MY X to MY Y" → recommend **A**; "my customers / embed / widget / let my customers connect their own accounts" → recommend **B**; **default to A (single-tenant)** when there's no ship-to-customers signal. **ALWAYS surface this as an explicit question — never infer it silently, even when the framing looks obvious** (a wrong silent guess means the wrong manifest scope and, on Path B, cross-tenant config bugs). Be *smart, not silent*: pre-select your recommended path as the default so accepting it is one click, and let the user flip it. Read `references/multi-tenancy.md` before doing anything with the manifest, connections-with-choices, or installations.

## PREREQUISITES gate — missing connector, action, event, or connection

The workflow tools can only USE connectors. Creating connectors, adding actions/events, and managing connections happen through the **connector tools** (`create_connector`, `create_action`, `get_connect_url`, …) — follow the **connector-builder** skill. Whenever discovery or probing reveals a gap, **fix it first, then resume this flow exactly where you left off:**

- **Connector missing** (not in `list_connectors`) → build it per connector-builder (search_api_docs → create_connector → auth → actions → verify with execute_action), then return.
- **Action missing** (no update action for a write side, no list action for pagination) → add it with `create_action` / `create_actions_batch`, verify with `execute_action` until 200 + contracts match, then return.
- **Event missing** (`get_connector_events` lacks it) → add the webhook config + subscription code per connector-builder, verify with `execute_subscription`, then return.
- **No / broken connection → HARD STOP, tell the user immediately.** The moment a connector has no ACTIVE connection or an existing one is invalid — including a **401/403 at probe OR test time** — STOP and surface it to the user RIGHT AWAY; never quietly continue or design around it. Confirm auth with `list_connections` / `check_connection`. To fix it, hand the user a secure connect page via `get_connect_url` — they enter credentials THERE, never in chat, and it handles every auth type (API key / basic / bearer / OAuth) in one flow. **Never collect secrets in the conversation.** Wait until ACTIVE before continuing (`check_connection` / `list_connections`). **Only AFTER you've surfaced the blocker and given the user its `get_connect_url`** may you keep making progress without waiting: validate LOGIC under `mockMode: true`, then hand off clearly stating which connector still needs connecting (with its `get_connect_url`) and that one live probe is required before it ships. A live connection is required before the workflow ships — mock mode is never a substitute for connecting.
- **More than one connector with the same or similar name, none connected, no user clarification** → do NOT prompt the user to connect all of them. First ASK which connector they mean (name the candidates), then — only if that chosen connector lacks an ACTIVE connection — surface a connect prompt / `get_connect_url` for THAT ONE connector alone, never for the whole set.
- **More than one connection for a connector** → the platform silently uses `is_default` and **never asks** — so YOU must. When `list_connections` shows >1 ACTIVE connection for a connector the flow uses, ASK the user which one (default or other). Single connection → proceed silently. Pin the chosen non-default connection on the **manifest** via `set_connector_scope { scope: "SAAS", connectionId }` — **never hardcode `connectionId` in saved workflow code** (code pins are for throwaway probes only). Full rules + the "one connector, two connections in one flow" cases: `references/multi-tenancy.md` §4.

Building the missing piece is the DEFAULT, not a fallback: never silently design around a gap (polling because an event is missing, dropping a field because there is no update action) — surface it and build it, or state explicitly why you are working around it.

---

# PHASE 1 — PLAN  → read `references/plan.md`

Turn a vague request into a confirmed, business-approved plan, in this order: **UNDERSTAND → RECOMMEND → ASK only what's open.** **Discover** connectors (`list_connectors`); **understand** what's possible — analyze entities on every connector (`analyze_entities`) and check feasibility/gaps with the cheap no-record tools (`get_connector_methods` / `get_action_schema` / `get_connector_events`), plus at most ONE tiny bounded recon probe per side when a real sample sharpens a suggestion; **form a recommended plan with smart suggestions**; then go to the user — in ONE message, business language only — confirming your recommendation and asking only the genuine open questions. Decide every technical choice yourself.

Non-negotiables:
- UNDERSTAND before asking: `analyze_entities` (+ cheap action/event feasibility checks) FIRST — you arrive with a recommended plan and matched pairs; the user confirms. Never ask from a blank slate.
- VERIFY entities by DATA, not name — confirm each source entity actually holds records; treat an empty/zero result as a red flag to investigate (cross-check business logic, search sibling entities), never as proof there's no data. (Detail: `references/plan.md` Step 2.)
- CHECK EVENTS for real-time — during PLAN run `get_connector_events` on the source; **real-time propagation REQUIRES an event trigger.** If the source exposes the create/update/delete events, recommend the event + schedule pair; if it has **none**, an ongoing sync can only run as **schedule-only polling** — surface that latency tradeoff (name the interval) to the user in Step 4, **never silently downgrade** a real-time expectation to polling. (Detail: `references/plan.md` Step 2 & Step 4.)
- LEAD with recommendations + smart suggestions: propose direction/source-of-truth, scope filters from real fields, trigger topology, and the matching key as defaults with reasons; surface opportunities and risks/gaps (with the fix). Ask ONLY what you genuinely can't infer — if it's all inferable, just confirm the plan and move on.
- Ask ONLY business questions (entities, direction, source of truth, scope) — via the `AskUserQuestion` tool, batched in ONE call, your recommendation as the default option. Decide triggers, pagination, matching keys, transforms, and conflict rules yourself — **the one exception is a schedule flow's cadence/run-time (how fresh the data must be, and the run time/timezone for a daily/weekly job), which you ASK the user when you arm the schedule (BUILD step 7), never decide silently.**
- For PLAN recon, **prefer `run_code`** (a bounded read + light logic → a compact summary: distinct values, counts, key uniqueness) over a raw probe — more flexible and less output. Keep it bounded and never fan out; full field discovery (and the reusable `probeId` via `probe_connector`) is MAP.
- ONE message, no re-asking what they already said. Wait for answers before MAP.

# PHASE 2 — MAP  → read `references/mapping.md`  (the mapping sub-skill)

Probe the REAL fields, author plain **1:1** mappings (semantic match, leaf fields in field-path notation) + suggested filter conditions + a matching strategy (ongoing only), then call `propose_configuration` ONCE. It returns a **`reviewUrl`** where the user reviews / edits / approves in the browser; poll **`check_config_status`** until approved to get the **`configId`**. You do NOT hand-build the review UI — the tool hosts it.

Non-negotiables:
- Properties/metadata action FIRST (listProperties, getFields, …), then fall back to a list action. Probe the target via its CREATE action. Record each side's `probeId` + connector slug + action slug.
- **ONE `propose_configuration` call for the whole use case** — all entity pairs and both directions bundled in `directions`. Per direction pass `sourceProbeId`/`targetProbeId`, `sourceConnectorSlug`/`sourceActionSlug`, `targetConnectorSlug`/`targetActionSlug` (target = its CREATE action), `mappings`, `conditions`, `matchingStrategy` (ongoing only). Never a config per entity.
- Mappings are **plain 1:1 only** — semantic match, leaf fields, every mapping with a `reason` + `confidence`; a required target with no match → `confidence: "low"`. Do NOT bake combine/conditional/fixed/AI into them — the user adds those at `reviewUrl`.
- **`integrationScope` is parsed for the entity — keep it EXACTLY `<Source> <Entity> -> <Target> <Entity>`** (singular nouns, no qualifiers / plurals / parentheticals). The platform reads the entity from the last word of each half, so `"... ShipBob orders (ongoing, one-way)"` becomes entity `"one-way)"` and corrupts the config; sync mode goes in `matchingStrategy` / `settings`. And **never hand-author `configurationDecisions`** — the tool derives them from the target CREATE contract. (Detail: `references/mapping.md` Step 6.)
- Share the `reviewUrl`; poll `check_config_status` → use the returned `configId`. NEVER fabricate it.
- The `configId` routes to the test-case gate below — NOT straight to build.

**SCOPED MAP — single-system automations that carry business rules.** Same tools, smaller surface. Source and target are the SAME connector, and often the same entity (a Brightpearl order in, the same order updated). Probe that entity once and reuse the one `probeId` for both sides. Then:
- The rule's **filter** becomes `conditions.suggestedConditions` — consolidate one field into a single `in` / `not_in` rather than several `equals` (`{ field: "product.category", operator: "in", value: "X,Y", reason: … }`).
- The rule's **chosen value** is set by the user at `reviewUrl` as a **fixed value** (or enum/lookup **selection**) on the field the automation writes — the exact affordance the review page exposes for every target field ("Set a fixed value" / "Select an enum option"). Do **NOT** force it into `propose_configuration` as a mapping with `sourceField: ""` — an empty-source mapping locks that review row and blocks approval of the whole config (`references/mapping.md` Step 3 and the reshape rule). Instead, leave the field out of `mappings[]` and, in the message that accompanies the `reviewUrl`, name the field and your recommended value so the user sets it in one click; it then lives in the saved config as an editable selection they can retune (switch "Main" to another warehouse) without a code change. Where the field is an enum or lookup, source your recommended option from `probe_connector_values`, never a typed literal.
- `integrationScope` still follows `<Source> <Entity> -> <Target> <Entity>` — both halves naming the same system is expected here (`Brightpearl Order -> Brightpearl Order`).
- No `matchingStrategy` — nothing is being matched across systems.
Everything else is unchanged: one call, plain mappings, `reviewUrl` → `configId`, then the GATE. BUILD reads the rules with `fastn.config.get(configId)` instead of hardcoding them.

# GATE — confirm test cases for EVERY build (before BUILD; config or none)  → read `references/test-cases.md`

**This GATE runs for EVERY workflow you build — data-movement sync OR single-system automation — and its output is ALWAYS user-approved before BUILD.** It is per-workflow, not conditional on a config existing. For a data-movement flow it sits between MAP and BUILD (derive cases from the saved config); for a single-system automation it sits right after you understand the request and before BUILD (derive cases from the workflow's own logic — its trigger, each business-rule branch, the real action contract, edge cases). **You NEVER author test cases and self-validate them silently** — every case set goes through `submit_test_cases` → `reviewUrl` → user approval, no matter how small or single-connector the build. The one exception the old wording implied ("the approval step is scoped to multi-connector syncs, so a single-connector automation can self-validate") is WRONG and removed: writing real records against a live system (a refund, an order edit, a status change) is exactly where an unreviewed business-rule interpretation does damage, so it needs the user's eyes MORE, not less.

After mappings are approved and the config is saved (data-movement), or as soon as the single-system request is understood, and BEFORE building: build the **acceptance test cases for the whole use case** — a structured set (`scope` + `testCases[]` of `{ id, group, scenario, pass, mode: live|mock }` + optional `groups[]`) — and submit it with **`submit_test_cases`**, which returns a **`reviewUrl`** where the user reviews / edits / adds / removes cases and **approves in the browser** (you do NOT hand-render this UI). No fixed limit and no round-number target — **DERIVE the count from the surface area**: for a data-movement sync, from the config (one case per mapping-required-field, per transform branch, per condition pass/skip, plus create + update + idempotency + edge/failure per entity pair × direction); for a single-system automation, from the workflow's own logic (trigger fires/no-fire, each business-rule branch and threshold, the real action contract, idempotency, edge/failure — see SINGLE-SYSTEM AUTOMATIONS in `references/test-cases.md`). A real sync yields **dozens**, not ~10; an automation is smaller (~10–20) but still never skips the gate; a thin set is why bugs slip through. Assign each case's `mode` YOURSELF — never ask the user live vs mock/sandbox: use `mock` for logic breadth, with `live` kept to the minimum that proves the real-write boundary — every entity pair + direction needs at least one `live` happy-path, the real end-to-end write mock can't prove. **Share the `reviewUrl`, poll for approval, then use the APPROVED set the tool returns** (the user may have edited it — never build from your submitted version, never fabricate the approval). The **test-case approval is the final go-ahead to build**. Each `pass` must be a **verifiable outcome** — exact return counts (`errors === 0` and `created`/`updated` moved) plus, for a live write, a read-back of the target confirming the mapped fields — never "it ran" / "2xx". Before submitting, run the **CASE POWER checks** (`references/test-cases.md`): trace every case to the requirement it verifies (the user's own phrase, a mapping id, a condition), and walk the kill matrix — for every production defect class that applies to this build (swapped mapping, dropped relation on a 200, unmapped enum passthrough, inverted filter, page-1-only pagination, update-recreates, wrapper shape, stale config), name the case that would FAIL if that bug existed; no killer = the suite is underpowered, add it before submitting. At BUILD, every approved case must be bound to its flow(s) via `create_workflow`'s `testCases` (they show in the dashboard validation panel), and used as the acceptance criteria when smoke-testing. The set then lives on as the flow's **permanent regression suite** — re-run in full after every later modification (see the non-negotiable in PHASE 3 below).

# PHASE 3 — BUILD  → read `references/build.md` + `references/dynamic-config.md`  (+ `references/sandbox.md`, `references/workflow-patterns.md`)

Build the workflows, probe writes, test safely, bind triggers, and finish with the widget. Follow BUILD ORDER 1–7 in `references/build.md`. Consult `references/sandbox.md` for the `fastn.*` contract and `references/workflow-patterns.md` for sync design rules, event-trigger rules, and live-execution safety.

Non-negotiables:
- **Build the WHOLE use case autonomously.** Once the test cases are approved, build EVERY flow the plan needs — backfill, each sync direction, the scheduled reconcile, event handlers — end to end. **Decide the build order yourself** (backfill first, then ongoing); NEVER ask the user which flow to build first, and NEVER stop between flows to ask whether to continue. Report once when all flows are built, tested, triggered, and the widget is up. ("Autonomous" means deciding order and building NEW flows without asking — it does NOT license overwriting existing ones; see the next bullet.)
- **DON'T MODIFY EXISTING FLOWS — ask first.** `create_workflow` slugs UPSERT (an existing slug silently OVERWRITES that workflow). Before creating, `list_workflows` and check whether a flow related to this integration already exists (same slug, or one already handling this entity pair/direction). If so, **STOP and ASK** whether to update it in place, build alongside under a new slug, or leave it untouched — name what you found and what you'd change. Default to a fresh, uniquely-slugged workflow; never edit or overwrite a pre-existing related flow without explicit confirmation. This is a legitimate pause, distinct from the autonomous-build rule above. (BUILD step 5 in `references/build.md`.)
- PROBE every action (reads AND writes) with `run_code` before `create_workflow` — schemas lie; only a real 2xx proves it. **Probe writes with a REAL source record passed through the REAL mapping** (a dress rehearsal of the runtime sync), never hand-invented values — synthetic data can pass while real data fails on lengths, formats, and enums. (Detail: BUILD ORDER step 4 in `references/build.md`.)
- ONE execution shape per workflow. Paginate fully. Skip-on-error. Named return counts. Test with the SMALLEST bounded input, never empty input.
- **A flow isn't done until every signed-off test case passes ON THE RETURNED VALUE** — parse the result and assert its exact `pass` condition; a 2xx is never a pass. `errors > 0` (SKIP-ON-ERROR hides per-record failures in `errorDetails`, so a run that failed everything still returns 200), or all-skipped / zero-outcome on a happy path, is a FAIL — and every live write must be read back from the target to confirm the mapped fields landed. Mock proves logic only; it's blind to whether the target accepts the payload (BUILD step 6).
- **The suite is a REGRESSION GATE — every later modification re-arms it.** Any `update_workflow` / `edit_workflow_code` / config repoint / connector-action change re-runs the FULL attached test suite for every affected flow (not just the case covering the edit) before you report the change done; update stale cases, add cases for new behavior, and `save_validation`. A fix that passes its own case can silently regress another — the full re-run catches it; eyeballing the diff does not. "Edited and published" is never "done" — an unvalidated edit is how a working integration silently breaks. (THE SUITE IS A REGRESSION GATE in `references/test-cases.md`; RE-TEST AFTER EVERY CHANGE in `references/workflow-patterns.md`.)
- **The end state is a SYNCED integration, not a clean-looking run.** Every record eligible under the user-approved filters must actually sync with the data available in the source; skipping is legitimate ONLY as an approved condition, never as a coping strategy for a mapping/matching problem the agent should have fixed. Mass skips — or zero writes against a non-empty source — are a defect to root-cause before hand-off; deliver the integration error-free, so the user is never the one who finds unsynced records and reports the bug back. (SKIP IS A DIAGNOSTIC in `references/workflow-patterns.md`; completeness gate in BUILD step 6.)
- The only allowed pauses are live-safety: ASK before any test that writes more than a couple of live records, and before binding a schedule that starts writing on a cadence — **that pause is also where you ASK the user the schedule cadence/run-time (BUILD step 7), not merely confirm arming.** Do NOT ask whether the account is sandbox or live, or whether to test live vs mock — testing is live by default and the live/mock split is YOUR call: treat every account as production, keep live writes to the minimal happy-path anchors with the smallest input, and run everything else in mock (only an explicit user request for mock-only testing changes this). Clean up everything a test wrote, and report anything you couldn't delete as **orphan** test data in the final hand-off (Step 9).
- **EVERY build ends with exactly ONE widget for the whole use case** — single-tenant or multi-tenant, the widget is always created. Pass the approved `configId` when one exists, plus `workflowIds`/`triggerIds` for the flows it publishes and `connectorIds` they call. NEVER one widget per entity or per flow. All flows read that same single `configId`. The path (SECOND AXIS above) only decides the manifest scope: **Path B flips per-customer connectors to `MULTI_TENANT` via `set_connector_scope`; Path A leaves the manifest all-`SAAS`** — neither path skips the widget.
- **`fastn.envConfig` — read with `.get`, write with `.set`.** Workflows read a static per-env value with `await fastn.envConfig.get(key)` (resolves against `ctx.env`; pass an env_slug as the second arg to read a specific env). Workflows write/upsert with `await fastn.envConfig.set(key, value)` — one call creates OR updates, idempotent. Both calls are async — **always `await`** them (a missing `await` silently passes a Promise downstream so the workflow reads `"[object Promise]"`). `.set` requires the executing JWT to carry the `workflows.update` role. Not a secret store — tokens, API keys, and OAuth client secrets belong in `fastn.secrets`. Full runtime contract in `references/sandbox.md`; the BUILD-time check that verifies READ keys already exist (and the exception for write-only keys) is BUILD ORDER step 3 in `references/build.md`.
- **Close with a hand-off** telling the user how to test the flow and how to reach the widget (dashboard **Widgets** tab → **Configure**) — see Step 9 in `references/build.md`. The hand-off comes AFTER the PHASE 4 verification report, never instead of it.

# PHASE 4 — VERIFY (mandatory — every build AND every update)  → run the `workflow-verifier` skill

BUILD ends with tested workflows, bound triggers, and a widget — but a bound trigger has never been proven to fire, the widget has never been read back, and config liveness has never been exercised. PHASE 4 verifies the ARMED system, using the procedures in the **workflow-verifier** skill (`workflow-verifier/references/verify-matrix.md`). It is not optional and it re-arms on every later modification, exactly like the test-case regression gate.

What must be proven, per surface (full procedures in the verify matrix):
- **Every trigger fired and produced an execution.** Schedule: run-now → `eventId` → find the execution carrying it (`list_executions`, matching `requestHeaders["x-fastn-event-id"]` — or the `eventId` filter where available). App event: subscription ACTIVE + a payloadSchema-shaped synthetic event → correlated execution → the right code branch ran. Webhook: POST to the `triggerUrl` → correlated execution. A fire with no execution is diagnosed at the trigger's monitoring/DLQ, not shrugged off.
- **Widget readback**: workflows/triggers/connectors actually attached, config template linked, exactly one widget for the use case, publish fan-out count asserted.
- **Config liveness**: edit a marker value, re-run, the run reflects it (multi-tenant: resolved config is the tenant's clone).
- **One real `execute_workflow`** per flow (not just the sandbox): execution row completed, detail logs/trace clean.
- **Connections ACTIVE and env-config keys resolving** at hand-off time.
- **Failures**: diagnose from the execution detail (`errorCategory`/`fixSuggestion`/logs/trace), self-heal up to 3 attempts (code fix, config repoint, subscription retry, DLQ replay), re-run the FULL suite after any heal, then report what remains.

**PHASE 4 ends with the VERIFICATION REPORT** — rendered Markdown written for the use case's owner (template in the workflow-verifier skill): a plain-language verdict first, then What's running (evidence per surface), Data parity (reconciled counts + sample audit + mismatch table), Failures (fix applied/proposed), Blockers as a checklist the user can act on (webhook registration URL, missing connection, secret-gated ingest), Coverage, Cleanup, and Next steps. "Report once when all flows are built, tested, triggered, and the widget is up" means: emit this report. A build that skips it is not done.

---

# ⭐ GOLDEN PRINCIPLES (apply across all phases)

1. **The config is the contract — dynamic, not baked in.** The approved mappings, filters, conditions, and transform parameters live in the approved config (the `configId`). Sync workflows read it every run via `fastn.config.get(configId)` + `fastn.evaluator` (if useful, else your own logic). When the user edits mappings/filters in the widget or dashboard, the next run picks them up — **no code change, no redeploy.** Never bake mapping rules into code as literals, and never read them from `ctx.input`. (Full detail: DYNAMIC CONFIG in `references/dynamic-config.md`.)
2. **ONE config, ONE widget, for the whole use case.** The entire integration — every entity pair and both directions — is a single `configId` and a single widget bound to it. The object the user edits in the widget is the exact object every workflow reads back via `fastn.config.get(configId)`. Never split into a config or widget per entity or per flow.
3. **Business questions only.** Decide everything technical yourself; ask the user only what a business owner can answer.
4. **`ctx.input` is SCOPE only** (limit, maxPages, modifiedSince) — never business configuration.
5. **Live by default.** `run_code`, `test_workflow`, and `execute_workflow` hit real systems. Probe small, mock when unsure, clean up after, and gate risky writes behind the user.
6. **Fill the gap, don't design around it.** A missing connector/action/event/connection routes through connector-builder first, then resume.
7. **Default to SAAS / single-tenant (Path A); multi-tenancy is opt-in — but confirm, don't assume.** New connectors resolve as `SAAS` (the developer's own pooled connection). Only flip per-customer connectors to `MULTI_TENANT` (`set_connector_scope`) when the use case is genuinely "let my customers run this against their own accounts." Don't touch the manifest for internal automations — all-`SAAS` is already correct. **ALWAYS ask the user which — never infer it silently — with your recommended path pre-selected as the default** (`references/plan.md` Step 4); Path A is the recommended default when unstated, but the user still confirms it every time. **The widget is built either way** — only the manifest scope is path-dependent. (Full model: `references/multi-tenancy.md`.)
8. **Connection choice is the agent's job, not the platform's.** >1 connection for a connector → ask which; pin the answer on the **manifest** (`set_connector_scope`), never in saved code. Multi-tenant flows read config with `fastn.config.getByTemplate(templateId)` so each customer gets their own clone, and the agent authors the config **template** only — never a per-customer clone.

# FILE MAP

| File | When to open |
|---|---|
| `references/plan.md` | PHASE 1 — discover, understand (analyze + feasibility + optional bounded recon), recommend a plan with smart suggestions, confirm/ask only open business questions |
| `references/mapping.md` | PHASE 2 — probe, author 1:1 mappings + conditions + matching, `propose_configuration` → `reviewUrl` → `configId` |
| `references/test-cases.md` | GATE — integration acceptance test cases to approve between MAP and BUILD |
| `references/build.md` | PHASE 3 — from-plan-to-workflows, BUILD ORDER, widget, triggers |
| `references/dynamic-config.md` | PHASE 3 companion — the DYNAMIC CONFIG contract (`configId`, get vs getByTemplate, config shape in the flow, EnrichSpec/`paramDeps`) and ENV-CONFIG |
| `workflow-verifier` skill (`references/verify-matrix.md`) | PHASE 4 — fire-and-correlate every trigger, widget/config readback, real-execution checks, self-heal, Markdown REPORT |
| `references/sandbox.md` | Writing workflow code — the `fastn.*` contract, what doesn't work, tiers, direct HTTP execution |
| `references/workflow-patterns.md` | Designing workflow logic — sync rules, event triggers, real-execution safety, code updates |
| `references/multi-tenancy.md` | Path A vs B, the connector manifest, connection resolution, multiple connections, installations, config template/clone, widget internals, self-install testing — read before touching the manifest, connections-with-choices, installations, or widgets |

## Reference documents (load on demand)
Open ONE only when you reach the phase that needs it - each is a local file in this skill's `references/` directory. Do NOT load them all up front.
- `references/plan.md` - When in plan phase
- `references/mapping.md` - When in mapping phase or mapping fields between systems
- `references/build.md` - When in build phase or building workflows
- `references/test-cases.md` - When generating test cases for the integration
- `references/sandbox.md` - When building or learn about the sandbox in which the flow code is excuted to build flows reliably
- `references/workflow-patterns.md` - To know about different patterns for integration or workflows
- `references/multi-tenancy.md` - When implementing multi tenancy in flows
- `references/dynamic-config.md`
