# Getting Started

Everything you need to go from signup to a working embedded integration — what Fastn is, how it works, and your first end-to-end build.

Fastn is an embedded integration platform for SaaS companies. You embed it into your product, and your customers get native integrations with the apps they already use without you building or maintaining the overall infrastructure.

This section walks you through the platform from first principles to a working integration.

<figure><img src="/files/FrqCJLVfp0wcBd3HogV6" alt=""><figcaption></figcaption></figure>

### In this section

[**What is Fastn**](https://github.com/fastn-ai/docs/tree/main/getting-started/what-is-fastn.md) — Who the platform is for, what it replaces, and how the core product areas work together.

[**Platform Overview**](https://github.com/fastn-ai/docs/tree/main/getting-started/platform-overview.md) — How the platform is organized. Covers the four main sections (Home, Integrations, Activity, Settings), the onboarding steps, and the architecture underneath.

[**Signing Up as a SaaS Partner** ](https://github.com/fastn-ai/docs/tree/main/getting-started/signing-up-as-a-saas-partner.md)— Create your account, pass the validation pipeline, and get your API keys and dashboard access.

[**Your First Integration**](https://github.com/fastn-ai/docs/tree/main/getting-started/your-first-integration.md) — Build a working integration from scratch using Fastn's AI functionalities

If you already have a Fastn account and want to jump straight to building, go to[ Your First Integration](https://github.com/fastn-ai/docs/tree/main/getting-started/your-first-integration.md).


# Overview

What fastn does, who each part is for, and the path from signup to a live sync.

### The problem fastn solves

Every integration you ship yourself carries the same recurring cost: an OAuth app to register, tokens to refresh, per-customer credentials to store, an upstream API that changes without warning, and a support queue when a sync quietly stops. Multiply that by the number of systems your customers use.

fastn takes that whole layer. You keep the part your customers pay for.

### The five things you work with

**Connectors** are the systems your customers can authorise — a catalogue managed by fastn, plus any you create. A connector knows an API's actions, its auth methods, and its webhooks.

**Connections** are what you get when a specific customer authorises a specific connector. Credentials live encrypted on fastn's side, scoped to that customer.

**Workflows** are the code that runs. Each one is a JavaScript module — a file called `<slug>.js` that exports `export default async function(ctx)`. It reads from one system, transforms, and writes to another. The agent writes them; you review, test, publish and deploy.

**Triggers** decide when a workflow runs — an inbound webhook, a schedule, or an event from a connected system.

**Unified APIs** give you one canonical endpoint per business entity — contact, document, message — served by whichever provider each customer connected, so your code stops branching per vendor.

Around those five sit two surfaces: **Widgets**, the panel your customers see inside your product, and **Activity**, where you watch everything that happens.

### The shape of a build

1. Describe the integration to the **Agent**, or wire it by hand.
2. The agent picks or creates **Connectors** and handles authentication.
3. It drafts a **Workflow**, generates test cases, and shows you the diff.
4. You attach a **Trigger** so it runs on real events.
5. You publish a version and **deploy** it to an environment.
6. You add the integration to your **Widget** so customers can turn it on.
7. You watch it in **Activity** and set an **Alert** so failures are not silent.

### Two audiences, one platform

fastn is used by two different people at once, and the product is split accordingly.

| You are…                     | You work in…                                                          |
| ---------------------------- | --------------------------------------------------------------------- |
| The SaaS company using fastn | Build, Operate and Manage — the dashboard in these docs               |
| Your customer                | The embedded widget inside your product — they never sign in to fastn |

Inside the dashboard, what you can see and do depends on your role. The role vocabulary is **Platform Admin**, **Owner**, **Admin**, **Developer** and **Operator**, and it changes the Settings navigation as well as individual permissions — an Owner or Admin sees People, General, Billing, Roles and Audit log; a Developer does not, but does see Trash.

{% content-ref url="/pages/BbqnvBllCaMbj80jWV6U" %}
[Platform tour](/fastn/readme/platform-tour)
{% endcontent-ref %}

{% content-ref url="/pages/KfUo3n2dw1Rm22Z4TOb6" %}
[FAQs](/reference/faqs)
{% endcontent-ref %}


# Platform tour

A screen-by-screen walk through the dashboard, so you know where everything lives.

### The chrome

The top bar carries five things that follow you everywhere.

| Element                  | What it does                                                                                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Search** (⌘K / Ctrl+K) | Placeholder *Search connectors, workflows, customers*. Results group under CONNECTORS, WORKFLOWS and CUSTOMERS.                                                                   |
| **Connect to Claude**    | Attaches this workspace to an MCP client so an assistant can call your connectors.                                                                                                |
| **Documentation**        | Opens these docs in a new tab.                                                                                                                                                    |
| **Theme**                | Switches the dashboard between light and dark.                                                                                                                                    |
| **AI credits**           | Reads *AI credits: n of m remaining this month*. Click it for a breakdown by agent and an org total, plus the reset date — quota resets at the start of each calendar month, UTC. |

The account card at the bottom-left switches organisation and opens your profile.

### Where everything lives

| Group   | Item            | Route                           |
| ------- | --------------- | ------------------------------- |
| —       | Home            | `/`                             |
| BUILD   | Integrations    | `/integrations?tab=connectors`  |
|         | Connectors      | `/integrations?tab=connectors`  |
|         | Unified APIs    | `/integrations?tab=unified`     |
|         | Connections     | `/integrations?tab=connections` |
|         | Workflows       | `/integrations?tab=workflows`   |
|         | Triggers        | `/integrations?tab=triggers`    |
|         | Pending updates | `/integrations/updates`         |
|         | Widgets         | `/widgets`                      |
| OPERATE | Activity        | `/activity/*`                   |
|         | Customers       | `/settings/customers`           |
| MANAGE  | Settings        | `/settings/*`                   |
| —       | Profile         | `/profile`                      |

{% hint style="info" %}
The sidebar parent **Integrations** links to its connectors view (`/integrations?tab=connectors`), and its badge shows the number of connectors in your catalogue. **Agent** is the first item under Integrations, at `/agent`; the **What do you want to build?** prompt on Home opens the same screen. Both are worth knowing before you go looking for a page that seems to have moved.
{% endhint %}

### Home

<figure><img src="/files/uNk5RVrtSIpkultEn3KC" alt="The fastn Home screen: a time-of-day greeting, one What do you want to build? prompt box, four suggestion chips, and a Connect to Claude button"><figcaption><p>Nothing else competes for attention here — no counters, no recent activity.</p></figcaption></figure>

One prompt box: *What do you want to build?* Type an integration in plain words and fastn routes you to the right agent. Four suggestion chips sit underneath — *Sync TikTok settlements*, *Notify Slack on orders*, *Log errors to Google Sheets* and *Build a Notion connector* — each a starting point you can take as-is or rewrite. **Connect to Claude** sits below them.

### BUILD → Integrations

The BUILD group holds two nav items: **Integrations**, which expands into the six pages below, and **Widgets**.

<figure><img src="/files/L7m71FNQxZhSfr9lip3F" alt="Integrations → Connectors showing 24 of 354 catalogue cards — AbstractAPI Email Reputation, AbstractAPI Holidays, Adyen, Agile CRM, Aha! — each badged managed and Managed by Fastn, with Import and Create connector top-right"><figcaption><p>Integrations → Connectors, filtered by connection state, auth type and visibility.</p></figcaption></figure>

| Page                | Purpose                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Connectors**      | Every system your customers can authorise. Create, import, connect.                                           |
| **Unified APIs**    | One canonical endpoint per business entity, backed by whichever providers you connect.                        |
| **Connections**     | Authenticated links between a customer and a system, with auth type and status.                               |
| **Workflows**       | The JavaScript that runs, with editor, tests, executions and API details.                                     |
| **Triggers**        | Webhooks, schedules and app events that start workflows.                                                      |
| **Pending updates** | Vendor changes to your integrations, and the fixes proposed for them. Nothing is applied until you accept it. |

**Connect to Claude** — in the top bar, and again on Home — exposes all of this to an AI client. See [MCP gateway](/build/mcp-gateway).

### BUILD → Widgets

<figure><img src="/files/xbkDHWwVrs73NZEllwSu" alt="Widget builder, Layout tab, badged Live, beside a Tablet-width preview: purple Integrations header, Connect your favorite tools, a search box, and TikTok Shop marked Not connected"><figcaption><p>Left: what you configure. Right: exactly what your customer sees.</p></figcaption></figure>

The widget builder has four tabs — Layout, Style, Features, Embed — and a live preview that renders at mobile, tablet and desktop widths. A sticky footer carries **Reset** and **Save and publish**; a **Live** badge appears once the widget is saved, and **Widget actions** offers **Reset to defaults**.

### OPERATE → Activity

<figure><img src="/files/QNbWUkDGunPT5Zsoug4X" alt="Activity → Events listing repeated Perigon article-alert webhook rows, each tagged webhook, Delivered, timestamped Sep 4, with a Replay link, under chips All 20, Webhook 20, Scheduled 0, Manual 0"><figcaption><p>Activity → Events, the inbound and outbound record.</p></figcaption></figure>

| Page             | Answers                                                         |
| ---------------- | --------------------------------------------------------------- |
| **Events**       | What arrived, from where, and was it delivered?                 |
| **Traces**       | Which connected systems did a run call, and how slow were they? |
| **Alerts**       | What should page us, and where?                                 |
| **Executions**   | Which runs happened, and how did they end?                      |
| **Sync reports** | What actually changed, record by record?                        |

### OPERATE → Customers

<figure><img src="/files/pCUS3D9bx04vwI4jgkgz" alt="The Customers list holding a single row — testing, identifier testing, 1 connection, Active — beside an All tiers filter, with Create customer top-right and View connections on the row"><figcaption><p>Every customer using your embedded integrations; this workspace has only a test one.</p></figcaption></figure>

**Customers** appears as its own item in the OPERATE group, a sibling of Activity rather than a page inside it — though its route is `/settings/customers`, not `/customers`. It lists everyone using your embedded integrations, with columns *Customer*, *Connections* and *Status* — `Active` or `Pending admin activation` — plus **Create customer**, a search box, and **View connections** on each row. Documented with [Operate](/operate/customers).

### MANAGE → Settings

**The Settings navigation is scoped to your role.** What you see there depends on who you are in this organisation:

| Role              | Settings contains                                                                                                      |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Owner / Admin** | People, General, API keys, Secrets, Environments, Configs, Database, SaaS Connectors, Billing, Roles, Audit log, Trash |
| **Developer**     | API keys, Secrets, Environments, Configs, Database, Trash                                                              |

Trash is in the sidebar for Owners, Admins and Developers alike, at `/settings/trash`. Two capabilities are gated by role rather than by permission — using the AI assistant, and reading the audit log. Each screen has its own page under [Settings](/manage/manage).

<figure><img src="/files/FpreBsAbBISaflSmSZZJ" alt="Settings → General: an Organisation block holding the organisation Name and Timezone Asia/Karachi, then Joining by email domain with an empty domain field and Approve automatically off"><figcaption><p>General is where Settings opens for an Owner — organisation identity first, joining rules second.</p></figcaption></figure>

### The account card

Bottom-left. It shows who you are and which organisation you are in. The chevron opens [your profile](/manage/profile); the up-down arrows open **SWITCH ORGANISATION**, which lists every organisation you belong to with your role in each — Platform Admin, Owner, Admin, Developer or Operator. Everything else in the dashboard is scoped to whichever organisation is selected, so check this first when something you expected to find is missing.


# Quickstart: your first integration

From an empty workspace to a workflow running on a real trigger.

This walks the shortest honest path. Budget about twenty minutes.

### 1. Describe what you want

Open **Integrations → Agent** in the sidebar, or type what you want to build into the **What do you want to build?** prompt on Home. Both land on the same screen.

<figure><img src="/files/PGlM71d3tbrZAHBl0hqr" alt="The Agent screen: the Sessions rail on the left, a Build an integration pane with four START FROM AN EXAMPLE cards, and a message composer with an Auto approval-mode chip"><figcaption><p><strong>Agent</strong> is the first item under Integrations; Home's prompt box opens the same place.</p></figcaption></figure>

The pane states the contract plainly:

> Describe what you need in plain words. The agents pick the connectors, draft the workflow, and show you the diff before anything runs.

If you would rather start from a shape than a blank page, tap one of the example starter prompts beneath the box — these are seeded suggestions and vary by workspace — and edit it to fit.

Otherwise, write the integration the way you would explain it to a colleague:

> When a HubSpot deal moves to closed-won, create a customer and a draft invoice in QuickBooks, and post a line in our #sales Slack channel.

Name the systems, the trigger, and the fields that matter. The agent asks about anything ambiguous rather than guessing.

The **Approval mode** chip under the message box decides how much it does unattended: **Auto** is the default and does not ask; **Manual** asks before any create, update or delete. Leave it on Auto while you are exploring.

### 2. Let it set up connectors and auth

The agent checks whether connectors exist for the systems you named, creates any that are missing, and handles authentication in the chat — API-key fields inline, or an OAuth form with client ID, secret and pre-filled scopes.

Anything it creates shows up under [Connectors](/build/connectors) afterwards, so you can inspect it.

### 3. Review the draft

The agent produces a workflow and opens the editor: configuration on the left, the tool tabs on the right, and — only where code editing is enabled — the code between them. **Code editing is off in almost every workspace**, so expect two columns rather than three; the workflow is still there, it is just written and updated by the agent rather than by you.

<figure><img src="/files/GcKrSJ1almOCcH8NvgFq" alt="The workflow editor showing the flow diagram"><figcaption><p>The Diagram tab draws the workflow from the code — it is read-only, and it cannot drift.</p></figcaption></figure>

Work through it in this order:

1. **The code** — where code editing is on, the middle column holds `<slug>.js`, a JavaScript module exporting `export default async function(ctx)`. This is the workflow; everything else on the screen describes, tests or deploys it. Where it is off, read the **Diagram** tab instead to see what the agent wrote.
2. **Diagram → Flow** — a read-only picture auto-generated from that code, with node kinds `TRIGGER`, `DECISION`, `READ` and `DONE`. You cannot edit the graph; edit the code and the graph follows.
3. **Contract** — check the input and output shapes.
4. **Connectors** — the list is extracted from the `fastn.connectors.X.Y(…)` calls in your code when you save. Confirm the right actions are wired, and whether each connector is marked **Per customer**.
5. **Configuration** (left panel) — set the execution tier and timeout. **Instant** is synchronous and capped at 30 seconds, **Standard** is asynchronous and capped at 15 minutes, **Long** is asynchronous and capped at 36 hours. Instant is the default; most syncs want Standard.

{% hint style="info" %}
Code editing is switched off in almost every workspace — it is enabled only for the parent organisation. There, workflows are generated and updated by the AI builder, and you can still test them, wire connectors, edit the contract, publish and deploy. If you want to write workflow code yourself, ask fastn to switch it on.
{% endhint %}

### 4. Test it

<figure><img src="/files/yp1apUmhvzKQOecdnSIY" alt="The Test tab with ctx.input and ctx.headers"><figcaption><p><strong>Use contract</strong> fills <code>ctx.input</code> with a sample built from the workflow's own contract.</p></figcaption></figure>

Open **Test**, click **Use contract** to populate a sample `ctx.input`, and choose a mode beside the run button: **Live** calls the real systems, **Partial Mock** mixes real calls with stubs, **Fully Mock** uses stubs only. Then hit **Run Live** (or **Run**).

If something is wrong, tell the agent rather than patching by hand. *"Skip deals under $500"* or *"Add error handling when QuickBooks is down"* and it rewrites the code, mappings and test cases together.

### 5. Publish and deploy

In the left panel, under **PUBLISH & DEPLOY**:

* **Publish snapshot** freezes the current code and configuration as a version. The workflows list numbers them in its **Latest** column (`v1`, `v2`, …), and shows `Unpublished` until the first one exists.
* **Deploy to environment** sends that version to an environment so it starts handling real events.

Until a snapshot is published, the workflow's status reads `Not published` and every call returns `WORKFLOW_NOT_PUBLISHED`.

### 6. Attach a trigger

A workflow with no trigger only runs when you call it. Go to **Integrations → Triggers → Add trigger** and pick one:

<figure><img src="/files/IHLdBrz3V4gVS9C0wFau" alt="The Add a trigger dialog, headed What should start the workflow?, with Webhook, Schedule and App event as three rows, each explained in a sentence and ending in an arrow"><figcaption><p>Three trigger types. A workflow with no trigger only runs when you call it.</p></figcaption></figure>

For the HubSpot example, choose **App event**. The form is progressive: name it, pick the HubSpot connector, then pick a connection and an event, then add a route pointing at your workflow. Two things to know before you start — the connector cannot be changed after the trigger is created, and you cannot get past the connector step without an active connection. Without one the form stops you:

> No active connection found for this connector. Connect first to use it as a trigger source.

Full field-by-field detail is in [Triggers](/build/triggers).

### 7. Put it in front of customers

Open **Widgets**, click **Add** under INTEGRATIONS, and pick the integration you just built. Then use the **Embed** tab to drop it into your product — see [Embedding the widget](/embed/embedding).

### 8. Make failure loud

Go to **Activity → Alerts** and click **Turn on failure alerts** — one click turns on the two alerts most teams need. A sync that fails quietly for six hours is a support ticket you could have avoided. Alerts are checked every 15 minutes, and the editor autosaves: there is no Save button, and a new alert exists the moment you create it.

{% hint style="success" %}
Done. From here, [Core concepts](/fastn/readme/concepts) explains the model underneath, [Workflows](/build/workflows) covers the editor in full, and [MCP gateway](/build/mcp-gateway) covers exposing the same integrations to an AI client.
{% endhint %}

{% hint style="info" %}
Deleted a connector or workflow by mistake while exploring? It is in [Settings → Trash](/manage/trash), restorable with its slug and history intact. Trash appears in the Settings sidebar for Owners, Admins and Developers alike, at `/settings/trash`.
{% endhint %}


# Core concepts

The handful of ideas that everything else in fastn is built from.

### Organisation, customer, connection

An **organisation** is you — the SaaS company. It holds your connectors, workflows, widgets, people and settings.

A **customer** is one of your customers. Every customer is isolated: its connections, credentials and data never cross into another. In older fastn material and in some API parameters this is called a *tenant*.

A **connection** is one customer's authorised link to one system. It stores the credential, encrypted, and records which auth method produced it.

```
Organisation (you)
├── Customer A
│   ├── Connection → HubSpot   (OAuth)
│   └── Connection → Cin7 Core (API key)
└── Customer B
    └── Connection → Salesforce (OAuth)
```

Every connection has an id in the form `ucl:org_<org>:<env>:<connectorId>:<authId>:<tenant>` — the detail page shows it with the line *Pass this to the API to act as this customer.* When you call a workflow on a customer's behalf, the tenancy is carried in request headers: `x-end-org-id`, `x-end-org-ref`, `x-installation-id`, `x-fastn-connections` and `x-fastn-installation-config`.

### Connector versus connection

A **connector** is the definition: this API, these actions, these auth methods, this webhook config. A **connection** is an instance of it for one customer. One connector, many connections.

Connectors are either **managed by fastn** — maintained upstream, patched when the vendor changes something — or **custom**, created by you.

### Per-customer versus account-level connectors

Inside a workflow, each connector is wired one of two ways.

| Mode             | Credential used                                                    | Use for                                            |
| ---------------- | ------------------------------------------------------------------ | -------------------------------------------------- |
| **Per customer** | The connection belonging to the running customer                   | Anything touching customer data                    |
| Account level    | A single connection owned by your org, shared across the workspace | Your own systems — your Slack, your data warehouse |

Only the first of these is labelled in the UI: the workflow's Connectors tab shows a **Per customer** badge, and a connection's detail page shows `Scope: Account level` when it is shared across the workspace.

### Workflows are code

A workflow is a JavaScript module. The editor holds one file, `<slug>.js`, and it exports a single function:

```javascript
export default async function (ctx) {
  // ctx.input, ctx.headers, ctx.connectors
}
```

There is no node palette and no drag-and-drop step builder. The editor's Diagram tab draws a picture *from* that code, and is read-only.

### Execution tiers

Every workflow declares how long it may run and how it answers the caller.

| Tier         | Behaviour                       | Returns       | Timeout range | Default |
| ------------ | ------------------------------- | ------------- | ------------- | ------- |
| **Instant**  | Synchronous; the caller waits   | Result inline | 1s – 30s      | 30s     |
| **Standard** | Asynchronous, run via Temporal  | 202 Accepted  | 5s – 15min    | 2min    |
| **Long**     | Asynchronous, for large volumes | 202 Accepted  | 30s – 36h     | 15min   |

Pick Instant only when something is waiting on the answer. Most syncs are Standard.

### Test and live

fastn separates *what the code is* from *where it runs*.

* **Versions** are snapshots created by Publish, numbered v1, v2, … in the workflows list.
* **Environments** are where versions are deployed. `test` and `live` are built in and protected; you can add named ones such as `staging`, and mark any environment **Requires review** so promoting to it opens a pull request on a connected GitHub repository instead of deploying straight away.
* **API keys** carry a mode, `Test` or `Live`. A Test key is refused unless the caller sends `X-fastn-Test-Mode: true`.

{% hint style="warning" %}
Test mode is not a sandbox. A test key reaches the same live connections as a live key and causes the same real writes. It is a separate credential, not a safe one.
{% endhint %}

### Triggers

Three kinds, covered fully in [Triggers](/build/triggers).

* **Webhook** — an outside system calls a URL you give it.
* **Schedule** — a clock you set.
* **App event** — something changed in a connected system.

### Unified APIs

Where several providers do the same job — HubSpot, Salesforce and Zoho all hold contacts — fastn exposes one canonical endpoint per entity and routes to whichever provider that customer connected. Your code calls `/api/v1/unified/crm/contact` and does not branch on the CRM.

There are three categories today — CRM (`Account`, `contact`, `Note`), Documents (`Document`, `Document Content`) and Messaging (`Channel Message`, `Direct Message`). `Note` and both message entities are create-only. See [Unified APIs](/build/unified-apis).

### The agent

fastn's agents are not autocomplete. They discover API specs, create connectors, wire auth, propose field mappings, generate test cases and write the workflow, and they show you a diff before anything runs. You review; they build.

### The MCP gateway

Everything you build can also be exposed to an AI client as tools, with the same customer scoping and permission model. See [MCP gateway](/build/mcp-gateway).


# Tutorials

Step-by-step guided lessons organized by role — SaaS Admin, Developer, and End User.

These tutorials are hands-on walkthroughs that teach you how to use Fastn by building real things. Each tutorial has a clear outcome and by the end, you'll have something working.

**Prerequisites:** Complete the [Getting Started](https://docs.fastn.ai/~/revisions/aj5eaS57qHvafrgYtfbL/fastn-v2/getting-started) section first. These tutorials assume you have a Fastn account and basic familiarity with the dashboard.

### SaaS Admin tutorials

For the person setting up and managing Fastn within their SaaS product.

[**Setting Up Your Organization** ](/tutorials/saas-admin/setting-up-your-organization)— Configure org settings, API keys (Test & Live), secrets, environments, OAuth Apps, and invite your team.

[**Creating a Workflow via AI**](/tutorials/saas-admin/creating-a-workflow-via-ai) — Use "Build with AI" to create workflows by describing what you need in plain language.

[**Building Your Widget**](/tutorials/saas-admin/building-your-widget) — See how widget builder works and how it can work for your customers

[**Managing Customers**](/tutorials/saas-admin/managing-customers) — Create customers, scope API calls, customize quota limits, and monitor activity.

[**Roles & Permissions Setup**](/tutorials/saas-admin/roles-and-permissions-setup) — Assign system roles (Owner, Admin, Developer, Operator, Viewer, End User), create custom roles, and manage access.

### End User tutorials

For the people who use integrations through the embedded widget in a SaaS product.

[**Connecting Apps via Widget** ](/tutorials/end-user/connecting-apps-via-widget)— Find available integrations, authorize your accounts, and activate connections.

[**Customizing Your Integrations**](/tutorials/end-user/customizing-your-integrations) — Configure sync preferences, field mappings, and scheduling.

[**Viewing Sync Status & History** ](/tutorials/end-user/viewing-sync-status-and-history)— Check what's syncing, view history, and troubleshoot failures.


# SaaS Admin

Tutorials for setting up and managing Fastn within your SaaS product organization config, connectors, workflows, widgets, tenants, and access control.

These tutorials are for the person responsible for Fastn inside a SaaS company typically are at a COO role or overseeing a SaaS product as a product manager or do not want to get in deep with the technicalities. You'll configure the platform, preview and publish the integration experience, and manage your customers' environments.

**Prerequisites:** Complete the Getting Started section first. These tutorials assume you have a Fastn account and have completed the Setup Assistants on the Home page.

### Tutorials in this section

[**Setting Up Your Organization**](/tutorials/saas-admin/setting-up-your-organization) — Configure organization settings, API keys (Test & Live), secrets, environments, and invite your team via Settings → People.

[**Creating a Workflow via AI**](/tutorials/saas-admin/creating-a-workflow-via-ai) — Use the "Build with AI" button to create workflows by describing what you need in plain language.

[**Building Your Widget**](/tutorials/saas-admin/building-your-widget) — An overview of how to customize, preview and publish your widget for your customers.

[**Managing Customers**](/tutorials/saas-admin/managing-customers) — Create customers under Settings → Customers, scope API calls per customer, and monitor customer-level activity.

[**Roles & Permissions Setup** ](/tutorials/saas-admin/roles-and-permissions-setup)— Assign system roles (Owner, Admin, Developer, Operator, Viewer, End User), create custom roles, and manage team access.


# Setting up your organization

Configure your organization profile, environments, API keys, and project settings after your first Fastn login.

After the Setup Assistant configures your core integrations, workflows, and widget, there are a handful of organization-level settings that allows you to control your company profile, team access, customer management, and billing. This tutorial walks through each one.

### Step 1: Configure organization settings

1. Click **Settings** in the top nav.
2. Click **General** in the left sidebar.

Here you can configure:

**Organization Name** — How your company appears across the platform.

**Timezone** — Used for scheduled triggers and activity timestamps.

**Domain & Access Control** — Set your email domain (e.g., `yourcompany.com`) so team members signing up with that domain can request to join. Toggle **"Auto-approve domain users"** to skip manual approval for teammates with matching email domains.

#### Company Research Report

During onboarding, the Setup Assistant's AI researched your company automatically and analyzed your existing integrations, customer ecosystem, competitive landscape, and integration gaps. The results are saved here.

Scroll down on the General page to find the **Company Research Report** card.

This report is what informed the AI's connector recommendations and workflow suggestions during onboarding. Revisit it when planning new integrations or evaluating which connectors to prioritize next.

> **Screenshot:** Settings → General showing Organization Name, Timezone, Domain & Access Control, and the Company Research Report card with the "View research report" button.

#### Reset Onboarding

At the bottom of the General page: **Reset Onboarding**. This reruns the Setup Assistant from scratch.

{% hint style="danger" %}
This is irreversible. It deletes your qualification wizard answers and every Setup Assistant thread. Only use this for testing or if you need to completely restart your onboarding configuration.
{% endhint %}

### Step 2: Review OAuth Apps

The Setup Assistant may have already configured OAuth for your connectors during onboarding when the AI set up inline auth, those configurations were saved here.

1. Go to **Settings → OAuth Apps**.
2. You'll see the list of platforms Fastn manages OAuth for.

Fastn manages OAuth apps on your behalf for common platforms, meaning you can authenticate connectors without creating your own OAuth application in the third-party's developer portal. If your connector's platform isn't listed, your development team will need to create an OAuth app in the third-party's developer settings and configure it here.

> **Screenshot needed:** Settings → OAuth Apps showing the managed OAuth apps list with platform icons and status indicators.

### Step 3: Invite your team

1. Go to **Settings → People**.
2. Click **"Invite User"**.
3. Enter their email address.
4. Select a role.
5. Click send andhey'll receive an email to join.

The People page shows a table with columns: **USER, ROLE, TEAMS, STATUS, LAST ACTIVE**. Filter by role, status, or team.

#### Roles

Fastn has six system roles. Each role has a fixed set of permissions across Connectors, Connections, Workflows, Agents, and Tools:

| Role          | Permissions | What they can do                                                                                                      |
| ------------- | ----------- | --------------------------------------------------------------------------------------------------------------------- |
| **Owner**     | 39          | Full access to everything an Admin can do, plus ownership transfer and organization deletion. One per organization.   |
| **Admin**     | 39          | Full access to connectors, workflows, customers, billing, settings. Cannot transfer ownership.                        |
| **Developer** | 34          | Allows a user to build connectors, workflows, agents. No billing or organization settings.                            |
| **Operator**  | 18          | Can run workflows, monitor activity. Cannot create or modify connectors or workflows.                                 |
| **Viewer**    | 7           | Read-only access across the platform.                                                                                 |
| **End User**  | 9           | Widget-only access. Can connect apps, view sync status, configure their own integrations through the embedded widget. |

To see the full permission breakdown or create custom roles: go to **Settings → ADVANCED → Roles**. Each system role can be duplicated as a custom role and modified. Click **"Create Custom Role"** to build one from scratch.

> **Screenshot needed:** Settings → People showing the user table with the Invite User button and role/status columns.

### Step 4: Manage customers

Your customers (the end users of your SaaS product who use the integrations) are managed under:

1. Go to **Settings → Customers**.
2. The page reads: "Manage customers under your account."

Each customer gets their own isolated environment i.e. their own connections, data, sync history, and configuration. When a customer connects an app through the embedded widget, they appear here automatically.

From this page you can view customer details, monitor their integration status, and set per-customer quota overrides.

> **Screenshot needed:** Settings → Customers page showing the customer list (or empty state for new accounts).

### Step 5: Check your plan and quotas

1. Go to **Settings → Billing**.
2. **Current Plan** shows your tier.
3. **Quota Usage** table shows each dimension with:
   * Plan default
   * Current usage (progress bar)
   * % used
   * Enforcement mode
   * Source

Key limits on the Free plan:

| Dimension            | Default | Enforcement |
| -------------------- | ------- | ----------- |
| Events per day       | 500     | Hard Block  |
| Events per minute    | 10      | Hard Block  |
| API calls per day    | 1,000   | Hard Block  |
| API calls per minute | 20      | Hard Block  |

Click **"Customize Customer Limits"** to set per-customer quota overrides which is useful when specific customers need higher limits than your plan default, or when you want to throttle individual customers to protect shared capacity.

> **Screenshot needed:** Settings → Billing showing the plan card and quota usage table with progress bars.

### Step 6: Review the Audit Log

1. Go to **Settings → Audit Log**.
2. The log shows a table with columns: **TIMESTAMP, USER, ACTION, RESOURCE, OUTCOME**.

Every significant action in your organization is recorded here from user logins, workflow deployments, connector changes to customer connections. Use it for compliance, debugging, or understanding who changed what.

> **Screenshot needed:** Settings → Audit Log showing a few example entries with timestamps and actions.

### For your development team

The Settings section also includes pages your developers will need:

* **API Keys** — Test and Live keys for programmatic access to the Fastn API
* **Secrets** — Sensitive values (third-party tokens, database credentials) that workflows read at runtime
* **Environments** — Separate configurations for development, staging, and production deployments

See more details in the [Developer](/tutorials/developer) tutorials for the full setup guide.

### What you've configured

* Organization name, timezone, and domain access control
* Reviewed the Company Research Report from onboarding
* Reviewed Fastn-managed OAuth apps
* Team members invited with appropriate roles
* Customer management set up
* Plan limits and quota enforcement reviewed
* Audit log available for compliance and debugging
* Developer team pointed to API Keys, Secrets, and Environments setup


# Configuring a connector

Set up a connector with OAuth or API key auth, configure its capabilities, test actions, and manage tenant-level credentials.

**Prerequisites:** [Your First Integration](https://github.com/fastn-ai/docs/tree/main/getting-started/your-first-integration.md).

### Two ways to create a connector

#### Manual setup

1. Go to **Integrations → Connectors**.
2. Click **+ Create**.
3. Fill in the Create Connector dialog:

| Field            | What to enter                                                        |
| ---------------- | -------------------------------------------------------------------- |
| **Name**         | Display name (e.g., "Salesforce")                                    |
| **Slug**         | URL-safe identifier, auto-generated from name (e.g., "salesforce")   |
| **Description**  | What the connector does (e.g., "CRM integration")                    |
| **Domain**       | The app's domain (e.g., "salesforce.com")                            |
| **Visibility**   | Private (only you) or shared                                         |
| **Icon URL**     | URL to the app's icon/logo                                           |
| **Auth Methods** | Select one or more from the dropdown, click **"+ Add"** for multiple |

4. Click **Create**.

> **Screenshot:** Create Connector dialog with all fields filled in.

#### Building with AI

1. Go to **Integrations → Connectors**.
2. Click **Build with AI**.
3. The **Connector Agent** page opens.
4. Describe what you want (e.g., "HubSpot CRM") or click a quick-start prompt.
5. The agent researches the API, discovers the spec, builds actions and events, and tests them.

> **Screenshot:** Connector Agent page with the chat input and quick-start prompts.

> **GIF needed:** Using Build with AI — entering a connector name, watching the agent discover the API and build actions.

### Authentication methods

Six auth methods are available when creating a connector:

| Method           | How it works                                                             | Example apps                          |
| ---------------- | ------------------------------------------------------------------------ | ------------------------------------- |
| **No Auth**      | No authentication required. For public APIs.                             | Public data feeds                     |
| **Basic Auth**   | Username and password sent as Base64 in Authorization header.            | Legacy APIs, internal systems         |
| **Bearer Token** | Token sent in Authorization header as `Bearer {token}`.                  | Many modern APIs                      |
| **API Key**      | Key sent in a custom header (e.g., `X-API-Key`).                         | Stripe, SendGrid, OpenAI              |
| **OAuth 2.0**    | User authorizes via the app's login screen. Fastn manages token refresh. | Slack, Shopify, HubSpot, Xero, Google |
| **Custom**       | A custom handler manages auth. For proprietary systems.                  | Internal tools, custom APIs           |

You can add multiple auth methods to one connector (click **"+ Add"** in the Auth Methods section). One method is set as default.

> **Screenshot:** Auth Methods dropdown showing all 6 options.

#### Using Fastn-managed OAuth apps

For supported platforms, Fastn manages OAuth on your behalf (see **Settings → OAuth Apps**). This means you can authenticate without creating your own OAuth application in the third-party's developer portal.

If the platform isn't listed under OAuth Apps, you'll need to:

1. Create an OAuth application in the third-party's developer portal
2. Copy the Client ID and Client Secret
3. Configure them in your connector's auth settings

### Adding a connection

A connector defines the integration. A **connection** is an authenticated instance — your credentials or your customer's credentials.

1. On the Connectors page, find your connector card.
2. Click **Connect** (first connection) or **+ Add Connection** (additional connections).
3. Complete the auth flow:
   * **OAuth 2.0:** Popup opens with the app's login screen. Authorize and you're redirected back.
   * **API Key / Bearer Token:** Paste the key/token and save.
   * **Basic Auth:** Enter username and password.
4. The connection appears under **Integrations → Connections**.

> **GIF:** Adding a connection — clicking Connect on a connector card, completing OAuth, seeing the connected state.

#### Connections vs connectors

|                   | Connector                                                 | Connection                                        |
| ----------------- | --------------------------------------------------------- | ------------------------------------------------- |
| **What it is**    | The integration definition (auth config, actions, events) | An authenticated instance with stored credentials |
| **How many**      | One per app                                               | Many — one per user/customer who connects         |
| **Where managed** | Integrations → Connectors                                 | Integrations → Connections                        |
| **Created by**    | You (manual or AI)                                        | You (for testing) or your customers (via widget)  |

#### Connector card states

On the Connectors page, each connector card shows:

| Element                     | Meaning                                    |
| --------------------------- | ------------------------------------------ |
| **PERSONAL** badge          | Created by you, not shared                 |
| **Connect** button (purple) | No connection yet — click to authenticate  |
| **+ Add Connection** button | Already connected — add another connection |
| **Created by: You**         | Shows who created the connector            |

### Connector visibility

| Visibility  | Who can see it                                   |
| ----------- | ------------------------------------------------ |
| **Private** | Only you and your organization                   |
| **Public**  | Available to other organizations (if applicable) |

Set visibility in the Create Connector dialog or edit it later from the connector's settings.

### What you've learned

* Two ways to create connectors: manual (+ Create) and AI (Build with AI)
* All 6 auth methods and when to use each
* How Fastn-managed OAuth apps simplify authentication
* The difference between connectors and connections
* Connector card states and visibility settings


# Creating a workflow via AI

An overview of building a workflow from natural language descriptions.

Fastn's AI agents build workflows from natural language descriptions. You can simply describe the workflow and what data to sync, between which systems, and the agent sets up the connectors, configures authentication, maps the fields, generates test cases, and then produces the workflow code with the addition of even visualizing it.

**Prerequisites:** [Your First Integration,](https://github.com/fastn-ai/docs/tree/main/getting-started/your-first-integration.md) by now you should know what workflows and triggers are.

### Where to start

You can build workflows with AI from two places in the platform:

**From the Home page** — The AI assistant on the Home page accepts natural language prompts like "Help me create an automation" or "Sync Salesforce leads to my CRM." This is the general-purpose entry point and will route you to the right agent.

**From the Integrations page** — Go to **Integrations → Workflows** and click **Build with AI** (next to "Create Workflow"). This opens the dedicated Workflow Builder, which is focused specifically on creating and validating workflows.

> **Screenshot needed:** Integrations → Workflows page with the "Build with AI" button highlighted next to "Create Workflow."

### Opening the Workflow Builder

1. Go to **Integrations → Workflows**.
2. Click **"Build with AI"** (next to the Create Workflow button).
3. The **Workflow Builder** page opens.

> **Screenshot:** Standalone Workflow Builder page showing the chat input, quick-start prompts, and the left sidebar with builder info and tasks.

### Describing what you need

Type a plain-language description of the workflow you want. The more specific you are about three things, the better the result:

**What data** — Name the systems and records involved. "HubSpot contacts," "Shopify orders above $100," "yesterday's Stripe payouts."

**What to do with it** — Describe the action. "Sync to Cin7 customers," "send a Slack notification to #orders," "generate a summary report."

**When it should run** — Describe the timing. "On every new order," "daily at 6 AM," "when a contact is updated."

#### Example prompts

**Data sync:**

> "Sync HubSpot contacts to Cin7 customers. Match by email. Create new Cin7 customers for any HubSpot contacts that don't exist yet."

**Notification:**

> "When a new Shopify order comes in, send a summary to the #orders Slack channel with the customer name, order total, and line items."

**Scheduled report:**

> "Every day at 6 AM, pull yesterday's Stripe payouts and send a summary to Slack with the total amount and number of transactions."

> **Screenshot needed:** Chat interface showing a user prompt (e.g., the HubSpot-to-Cin7 example) and the agent beginning to analyze the request.

### What the agent does

After you describe your workflow, the agent works through several stages. You'll see each one happen in the chat in the following order:

#### 1. Analyzes your request

The agent breaks down what you've asked for, this also includes which systems are involved, what data needs to move, what transformations are needed, and what trigger to use.

#### 2. Sets up connectors

If the systems you mentioned don't have connectors configured yet, the agent will flag this and either create one or ask you for credentials.

For example, if you ask for a HubSpot sync but haven't connected HubSpot yet, the agent will inform you that it needs to setup your connector before it can plan an integration with HubSpot.

#### 3. Handles authentication inline

When a connector needs credentials, the auth form appears right inside the chat so you never have to leave the conversation.

For **API key auth**, you'll see a tabbed form with fields for the key and any required configuration. For **OAuth** (e.g., HubSpot, Shopify), you'll see a form with fields for Client ID, Client Secret, and pre-filled OAuth scopes, plus a link to the provider's portal to get your credentials.

> **Screenshot needed:** Inline OAuth form in the chat showing CLIENT ID, CLIENT SECRET, OAUTH SCOPES fields, and a portal link.

#### 4. Maps fields between systems

Once connectors are ready, the agent generates field mappings between source and target systems. You'll see a **"WE'VE SET THIS UP FOR YOU"** banner with a summary of each mapping.

Each mapping row shows a plain-language description of what it does (e.g., "Contact email → Customer email"), the source field, the target field, and a **Change** button to modify it. Fixed values appear as colored badges, for example, a yellow badge showing `Format: "presentation"` or a green badge showing `Text Mode: "generate"`.

Below the mappings you'll find:

* **Record Matching Strategy** — How the workflow tracks records across systems (e.g., "Track IDs via fastn.state")
* **+ Add Mapping** — Add a field mapping the agent didn't include
* **Add Filters** — Narrow which records get synced
* A natural-language input where you can describe changes and the agent will update the mappings

When everything looks right, click **"Looks good, turn on"** to approve the field mapping configuration.

> **Screenshot needed:** Field mapping panel showing the "WE'VE SET THIS UP FOR YOU" banner, 3-4 mapping rows with source/target dropdowns and Change buttons, fixed value badges, and the "Looks good, turn on" button.

#### 5. Generates test cases

The agent creates test cases to validate the workflow before it goes live. You'll see a test case panel showing with named test groups.

Each test case has:

* A **MOCK** or **LIVE** badge

{% hint style="info" %}
Mock tests use simulated data whereas live tests hit the actual APIs
{% endhint %}

* A description of the scenario being tested
* A **Feedback** field where you can flag issues

You can easily review the test cases, approve the ones that look correct and then flag any that need changes and the agent will revise them.

> **Screenshot needed:** Test cases panel showing MOCK/LIVE badges, scenario descriptions, Feedback fields, and Approve buttons.

#### 6. Reports the result

The agent summarizes what was built from the workflow, the trigger, the field mappings to the test results. The workflow is now ready for deployment.

> **GIF needed:** Full sequence — typing a prompt, agent analyzing, setting up auth, showing field mappings, generating test cases, final summary. (This is the hero visual for this page — should be \~15-20 seconds showing the AI doing the work.)

### Iterating on the workflow

In case you want changes on your workflow, you can ask follow-up questions to refine what the agent built such as:

* "Add error handling for when the Cin7 API is down"
* "Filter out contacts without email addresses"
* "Change the schedule to every hour instead of daily"
* "Add a Slack notification when the sync fails"
* "Only sync contacts created in the last 30 days"

The agent then updates the workflow, field mappings, and test cases based on your follow-up. The **TASKS** panel on the left sidebar tracks what the agent has completed and what's still pending.

### Reviewing what the agent built

Once the agent finishes, the workflow appears in your **Integrations → Workflows** list. Click on it to open the Workflow Editor.

The editor has three panels. As a SaaS admin, the one that matters most is the right panel specifically the **Docs** tab.

#### Visualizing the workflow

The **Docs** tab has core sub-tabs: **Flow**, **Sequence**, and **Docs**.

The **Flow** sub-tab renders a visual flowchart of the entire workflow as a node graph. Its the very workflow in result that would have taken you manually to build without Fastn's agents. Every step the agent generated appears as a connected node consisting decision points, API calls, data retrieval, and skip/error paths are all mapped out visually.

You can use the visualizer to verify if the agent build the correct logic. This also allows you to trace the flow from triggger to completion to check for any missing branches or wrong conditions.

The **Sequence** sub-tab shows the same logic as a step-by-step timeline, useful for understanding the order things happen in.

The **Docs** sub-tab shows auto-generated documentation describing what the workflow does.

{% hint style="info" %}
This same visualization appears in the customer-facing widget when end users click into an active workflow. What you see here is what your customers see so reviewing it now also means reviewing your customers' experience.
{% endhint %}

> **Screenshot needed:** Right panel Docs tab with the Flow sub-tab active, showing the full visual flowchart with connected nodes, decision branches, and action steps.

#### What else is in the editor

The **left panel** shows the workflow's configuration i.e. its name, description, execution settings, and the **Publish & Deploy** buttons you'll use to go live. The agent sets these up for you, but you can adjust them if needed.

The **center panel** shows the generated code. The footer shows medata tags like `Edited via agent`, indicating that the code was produced by the AI agent.

### Tips for better results

1. **Be concrete about your prompts**\
   "Sync HubSpot contacts to Cin7 customers, match by email" works better than "integrate HubSpot and Cin7."
2. **Name the specific fields if you care about them.**\
   "Include the customer name, order total, and line items in the Slack message" gives the agent enough to build the right field mapping.
3. **Iterate rather than rewrite**\
   If the first result is 80% right, send a follow-up to fix the remaining 20%. The agent keeps context from the conversation.

### What you've learned

* How to open the Workflow Builder from Integrations and Build with AI
* How to describe a workflow in plain language so the agent can build it
* How the agent handles the full cycle: connector setup, inline auth, field mapping, test case generation
* How to visualize the workflow your agent built end to end
* How to iterate on the generated workflow with follow-up prompts


# Configuring a workflow in code

Write workflows in the code editor using export default async function(ctx), configure execution tiers, and test with sample input.

**Prerequisites:** [Your First Integration](https://github.com/fastn-ai/docs/tree/main/getting-started/your-first-integration.md). Basic JavaScript familiarity.

### Opening the code editor

1. Go to **Integrations → Workflows**.
2. Click **Create Workflow**.
3. The workflow editor opens with three panels.

> **Screenshot:** Full workflow editor showing all three panels — Configuration (left), Code (center), Test (right).

### The three panels

The code editor consists of three panels which are as follows:

#### 1. Configuration

| Field                 | What it does                                                                       |
| --------------------- | ---------------------------------------------------------------------------------- |
| **Name**              | Workflow identifier (required). Example: "sync-contacts"                           |
| **Description**       | What the workflow does. Shown in the workflows table.                              |
| **Execution Tier**    | **Instant** (sync, max 60s), **Standard** (async, default), or **Long** (extended) |
| **Execution Timeout** | Max time before timeout. Slider: 1s–2min range for Instant tier.                   |
| **Retry Policy**      | Toggle on/off. When on, failed executions retry automatically.                     |

#### 2. Code editor

The editor shows `workflow.js` with a default template:

```javascript
export default async function(ctx) {
  const { input, headers } = ctx;
  // Your workflow logic here
  return { result: "Hello from workflow!", input };
}
```

Every workflow exports a default async function that receives `ctx` — the context object.

#### 3. Test panel

Two tabs:

* **Test** — run the workflow with sample input
* **Docs** — reference documentation

The Test tab has:

* **CTX.INPUT** — JSON input your workflow receives
* **CTX.HEADERS** — optional HTTP headers
* **Run** button — executes the workflow without saving

### The ctx object

Your workflow receives a context object with:

```javascript
const { input, headers } = ctx;
```

| Property  | What it contains                                                                     |
| --------- | ------------------------------------------------------------------------------------ |
| `input`   | The incoming data — request body from a webhook, event payload, or scheduler context |
| `headers` | HTTP headers from the incoming request (if triggered by webhook)                     |

### Writing a workflow

#### Example: Process incoming contacts

```javascript
export default async function(ctx) {
  const { input } = ctx;
  
  const contacts = input.contacts || [];
  const processed = [];
  
  for (const contact of contacts) {
    // Transform the contact data
    processed.push({
      email: contact.email?.toLowerCase(),
      name: `${contact.firstName} ${contact.lastName}`.trim(),
      source: "hubspot",
      syncedAt: new Date().toISOString()
    });
  }
  
  return {
    result: "success",
    count: processed.length,
    contacts: processed
  };
}
```

#### Example: Conditional logic

```javascript
export default async function(ctx) {
  const { input } = ctx;
  const order = input.order;
  
  if (!order) {
    return { error: "No order data provided" };
  }
  
  if (order.total > 500) {
    // High-value order — needs review
    return {
      action: "review",
      reason: `Order total $${order.total} exceeds threshold`,
      order
    };
  }
  
  // Standard order — process normally
  return {
    action: "process",
    order
  };
}
```

### Testing

1. Click the **Test** tab in the right panel.
2. Enter sample JSON in **CTX.INPUT**:

```json
{
  "contacts": [
    { "email": "Jane@Acme.com", "firstName": "Jane", "lastName": "Doe" },
    { "email": "john@widget.co", "firstName": "John", "lastName": "Smith" }
  ]
}
```

3. Optionally add headers in **CTX.HEADERS**.
4. Click **Run**.
5. The output appears below — check the result matches your expectations.
6. Toggle between **Test** (sample data) and **Real** mode if available.

> **Screenshot:** Test panel with sample input JSON and the output result after running.

### Choosing an execution tier

| Tier         | Behavior                                                                          | Use when                                                             |
| ------------ | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **Instant**  | Synchronous — caller waits for the result. Max 60 seconds. Returns result inline. | API endpoints that need a response, quick lookups, simple transforms |
| **Standard** | Asynchronous — returns immediately, executes in background.                       | Data syncs, multi-step processes, most workflows                     |
| **Long**     | Extended async — for large data volumes and long-running processes.               | Batch imports, full data backfills, large report generation          |

The Workflows table has filter tabs for **All | Instant | Standard | Long** so you can filter by tier.

### Creating the workflow

Once your code works in testing:

1. Fill in **Name** and **Description** in the Configuration panel.
2. Select the appropriate **Execution Tier**.
3. Set **Execution Timeout** (default: 1 minute for Instant).
4. Toggle **Retry Policy** if you want automatic retries on failure.
5. Click **Create Workflow** (bottom of the Configuration panel).

The workflow appears in the Workflows table with status **active**.

> **Screenshot:** Workflows table showing the newly created workflow with active status.

### Editing an existing workflow

1. Go to **Integrations → Workflows**.
2. Click on the workflow name in the table.
3. The code editor opens with the existing code.
4. Make your changes, test them, then save.

### What you've learned

* The three-panel code editor layout (Configuration, Code, Test)
* The `ctx` object structure (`input`, `headers`)
* How to write workflow logic in JavaScript
* How to test with sample data before creating
* How to choose the right execution tier
* How to create and edit workflows


# Setting up triggers

Create webhook, scheduler, and app event triggers under Integrations → Triggers and route them to your workflows.

Triggers are what start your workflows. In Fastn, triggers are managed **separately from workflows,** you create a trigger under **Integrations → Triggers**, then route it to one or more workflows.

1. Go to **Integrations → Triggers**.
2. The page has three tabs:
   * **Webhooks** — HTTP POST endpoints for external events
   * **Schedulers** — Time-based triggers (interval, daily, weekly, monthly)
   * **App Events** — Subscribe to events from your connectors
3. Click **Add Trigger** to create a new trigger.
4. Select the trigger type: **Webhook**, **Scheduler**, or **App Event**.

> **Screenshot:** Triggers page showing the three tabs and the Add Trigger type selection panel.

### Webhook triggers

Webhooks receive events from external services via HTTP POST. When a third-party app sends a webhook to your endpoint, the trigger routes the payload to your workflow.

#### Creating a webhook trigger

1. Click **Add Trigger** → **Webhook**.
2. Enter a **Name** and optional **Description** ("Describe what this webhook receives...").
3. Configure **Routes**:

Routes map incoming webhooks to workflows. Each route has:

| Field           | What it does                                                                 |
| --------------- | ---------------------------------------------------------------------------- |
| **Workflow**    | Select the target workflow from the dropdown                                 |
| **Key / Value** | Optional JSON filter — only payloads matching this filter trigger this route |
| **Header**      | Optional headers to forward to the workflow                                  |

4. Click **+ Add route** to add more routes. A single webhook trigger can route to **multiple workflows** based on different payload filters.
5. Expand **Advanced options** for additional configuration.
6. Click **Create**.

> **Screenshot needed:** Webhook trigger configuration showing Routes with workflow dropdown, Key/Value filter, and Header fields.

#### Example: Route by event type

A single webhook endpoint receiving different event types:

**Route 1:**

* Workflow = "process-new-orders"
* Filter Key = `event_type`
* Value = `order.created`

**Route 2:**

* Workflow = "update-inventory"
* Filter Key = `event_type`
* Value = `inventory.updated`

Different payloads go to different workflows through the same webhook URL.

### Scheduler triggers

Schedulers trigger workflows on a time-based schedule.

#### Creating a scheduler trigger

1. Click **Add Trigger** → **Scheduler**.
2. Enter a **Name** (required) and optional **Description**.
3. Configure the **Schedule** using preset buttons:

| Preset       | Configuration                                   |
| ------------ | ----------------------------------------------- |
| **Interval** | Run every X minutes/hours. Set number and unit. |
| **Daily**    | Run once per day at a specific time.            |
| **Weekly**   | Run once per week on a specific day and time.   |
| **Monthly**  | Run once per month on a specific date and time. |
| **Custom**   | Enter a custom cron expression.                 |

4. For **Interval**: set "Run every \[number] \[minutes/hours]".
5. Optionally set **Starts at** — a date/time to begin. Leave empty to start immediately.
6. Click **Create**.

> **Screenshot:** Scheduler trigger configuration showing the preset buttons (Interval, Daily, Weekly, Monthly, Custom) and the interval settings.

#### Example schedules

* "Run every 5 minutes" — Interval preset, 5 minutes
* "Daily at 6 AM" — Daily preset, 06:00
* "Every Monday at 9 AM" — Weekly preset, Monday 09:00
* "First of every month" — Monthly preset, day 1

### App Event triggers

App Events subscribe to events from your connected third-party apps. When something happens in the app (new sale, contact update), Fastn receives the event and triggers your workflow.

#### Creating an app event trigger

1. Click **Add Trigger** → **App Event**.
2. Enter a **Name**.
3. Select the **Connector** (e.g., "Cin7 Core") from the dropdown.
4. Select the **Connection** — which authenticated connection to use for this trigger.
5. Browse the **Event** list — shows available events from the connector:
   * Each event shows: event name, type badge (**WEBHOOK**), subscription status (**Not Subscribed** / **Subscribed**)
   * Event path (e.g., "Sale/Created")
   * Expandable **Payload Schema** showing the event data structure
6. Select the event you want to subscribe to.
7. Click **Create**.

> **Screenshot:** App Event trigger configuration showing the Connector dropdown, Connection dropdown, and the event list with event names, WEBHOOK badges, and subscription status.

#### Event subscription

When you create an App Event trigger, Fastn subscribes to the webhook on the third-party app automatically. The subscription status changes from "Not Subscribed" to "Subscribed". When you delete the trigger, Fastn unsubscribes.

### Managing triggers

#### Viewing triggers

Each tab (Webhooks, Schedulers, App Events) shows a list of triggers with:

* Search bar
* Status filter ("All statuses")
* Trigger count in the tab label (e.g., "Webhooks (0)", "Schedulers (1)", "App Events (3)")

#### Editing and deleting

Click on a trigger to edit its configuration, routes, or schedule. Delete triggers you no longer need — this stops the webhook/schedule and unsubscribes from app events.

### What you've learned

* Triggers are managed separately from workflows under Integrations → Triggers
* Three trigger types: Webhook (with routes and filters), Scheduler (with presets), App Event (with connector event subscription)
* A single webhook trigger can route to multiple workflows
* Schedulers use friendly presets (not raw cron by default)
* App Event triggers show subscription status and payload schemas


# Building your widget

Use the Widget Builder to create an embeddable integration hub add integrations, customize layout and style, and preview the customer experience.

The widget is what your customers see — an integration portal embedded inside your product where they connect apps, manage workflows, and monitor sync activity. You configure it visually in the Widget Builder; your development team handles the embed code separately.

**Prerequisites:** Complete [Your First Integration](https://claude.ai/fastn/getting-started/your-first-integration). During the Setup Assistant (Step 4: Embed), you may have already created your first widget. This tutorial covers building or customizing widget hubs beyond that initial setup.

***

### Opening the Widget Builder

Click **Widgets** in the top navigation bar (Home | Integrations | **Widgets** | Activity | Settings).

The Widget Builder opens with two panels: the **builder** on the left and the **live preview** on the right. Everything you change on the left updates the preview in real time.

> 📷 **Screenshot needed:** Full Widget Builder page showing the left builder panel and the right live preview panel side by side.

***

### Adding integrations to the hub

The left panel starts with an **INTEGRATIONS** section at the top. This controls which connectors and workflows your customers can access through the widget.

1. Click **+ Add** to add an integration.
2. Select from your configured connectors and workflows.
3. Each integration appears as a card with edit (✏️) and delete (🗑) icons.
4. Drag cards to reorder how they appear in the hub.

> 📷 **Screenshot needed:** INTEGRATIONS list showing 3-4 added integrations with their app icons, edit/delete icons, and drag handles.

***

### Configuring Layout

Click the **Layout** tab in the left panel. This controls the hub's content and structure.

#### Header Content

**Title** — The heading your customers see at the top of the widget (e.g., "Integrations").

**Subtitle** — A short description beneath the title (e.g., "Connect your favorite tools").

#### Use Templates

Click **+ Add** to add workflow templates that appear in the Workflows tab of the preview. Each template shows as a card with an × to remove it. When a customer clicks a template in the live widget, it launches an **Integration Agent** session that walks them through setting up the workflow — configuring connections, field mappings, and test cases.

> 📷 **Screenshot needed:** Layout tab showing Header Content (Title: "Integrations", Subtitle: "Connect your favorite tools") and the Use Templates section with template cards and the + Add button.

#### Widget Sections

Still within the Layout tab, scroll down to the **Widget Sections** panel. This controls which parts of the widget your customers see and in what order.

The subtext reads: **"Drag to reorder. Toggle to show/hide."**

Six sections are available, each with a drag handle (⠿) and a green toggle switch:

**Header & Branding** — The title bar at the top of the widget showing the title and subtitle you configured above. Toggle OFF to hide the header entirely (useful if your product already has its own header above the widget).

**AI Assistant** — A prompt area in the widget that reads "Can't find what you need?" with the subtext "Tell AI what you want to automate and it will configure it for you." Customers can type a request (e.g., "Sync Shopify orders to my CRM...") and click **Build with AI** to launch an agent session directly inside the widget. This is the customer-facing equivalent of the Build with AI button you use in the Integrations section.

**Search Bar** — A search field ("Search integrations across all categories...") that lets customers filter integrations by name or category.

**Apps** — The integration cards showing connected apps with Configure and Disconnect buttons.

**Workflows** — The active workflows list and template cards.

**Insights** — Performance metrics and KPI cards.

Drag sections to change the order they appear in the widget. Toggle any section OFF to hide it from customers entirely. For example, if you don't want customers building their own automations, toggle off **AI Assistant**. If you don't need workflow visibility, toggle off **Workflows**.

The live preview on the right updates immediately as you reorder or toggle sections.

> 📷 **Screenshot needed:** Widget Sections panel within the Layout tab, showing all six sections with drag handles and green toggles. Show the Reset and Save & Publish buttons at the bottom.

> 🎬 **GIF needed:** Dragging the "AI Assistant" section from its position to above "Apps," then watching the live preview reorder in real time.

> 🎬 **GIF needed:** Toggling off "Search Bar" and watching it disappear from the live preview.

***

### Customizing Style

Click the **Style** tab to match the widget to your product's look and feel. The Style tab has four sub-tabs:

#### Colors

Set the color palette for your widget. Available tokens include: primary, primary-foreground, background, foreground, card, muted, muted-foreground, and border. Adjust each to match your brand.

> 📷 **Screenshot needed:** Colors sub-tab showing the color token controls with color pickers.

#### Typography

Control font family, font size, font weight, and letter spacing for the widget text.

#### Shape

Fine-tune corner rounding with three separate radius controls, each with a slider and quick-select presets (**Square | Rounded | Full**):

* **Global Radius** — Applied to cards, panels, and dropdowns (e.g., 8px)
* **Button Radius** — Applied to all buttons (e.g., 8px)
* **Input Radius** — Applied to inputs and selects (e.g., 6px)

Below the radius controls: **Shadow Strength** with four presets — **None | SM | MD | LG**. This controls the drop shadow depth on cards and panels.

> 📷 **Screenshot needed:** Shape sub-tab showing the three radius sliders with Square/Rounded/Full presets and the Shadow Strength selector with MD selected.

#### Json

This sub-tab lets you export and import your full style configuration as code — useful for sharing with your development team or applying a consistent theme across multiple widgets.

**CSS Variables** — A read-only JSON block showing all your design tokens (colors, radii, typography) as CSS custom properties. Below it, a `:root { }` CSS block with a **Copy** button, labeled "Apply to your container element." Your developers can paste this directly into your product's CSS to match the widget's styling.

**Import from JSON** — Paste a tokens JSON object (e.g., `{ "--primary": "#4F46E5", ... }`) and click **Apply** to set all style values at once. This is useful when you have an existing design system and want to apply it without adjusting each control individually.

> 📷 **Screenshot needed:** Json sub-tab showing the CSS Variables block, the `:root` CSS output with Copy button, and the Import from JSON input area with Apply button.

> 🎬 **GIF needed:** Changing a color in the Colors sub-tab and watching the live preview update in real time.

***

### Features

Click the **Features** tab. A banner reads: **"Coming soon — these features are currently unavailable."**

Two features are listed with greyed-out toggles:

**Widget Filter** — "Let end users filter widgets by category or status." Not yet available.

**RBAC** — "Role-based access control per widget." Not yet available.

These toggles will become active when the features ship. No configuration is needed here for now.

***

### Using the Live Preview

The right panel shows exactly what your customers will see. Use the controls at the top to test different views:

**Viewport toggles** — Three device icons in the top-right of the preview panel let you switch between phone, tablet, and desktop layouts.

**Preview button** — The **⊡ Preview** button opens a full-page preview for detailed inspection.

**Live Preview status** — A green dot with "Live Preview" text in the top-left confirms the preview is syncing with your changes in real time.

The preview has three tabs:

#### Apps tab

Shows the integrations your customers can connect to. At the top, a search bar lets them search across all categories. Each integration appears as a card with:

* App icon and name
* Connection status (green dot + "Connected" label)
* **⚙ Configure** button (dark/filled) — opens the integration configuration dialog (see below)
* **Disconnect** button (outlined)

Below the integration cards, if the **AI Assistant** section is enabled, customers see: "Can't find what you need?" with a text input where they can describe what they want to automate and click **Build with AI** to start an agent session.

At the bottom: **"Powered by fastn"** footer.

This is the first thing your customers see when they open the widget.

> 📷 **Screenshot needed:** Apps tab in the preview showing connected integrations (e.g., HubSpot and Gamma with "Connected" labels), Configure/Disconnect buttons, and the AI Assistant prompt at the bottom.

#### Workflows tab

Shows two things:

**Active workflows** — Workflows currently running for this customer. Click the arrow on any workflow to open the **workflow visualizer** — an interactive node graph showing the full flow logic with TRIGGER, PROCESS, DECISION, READ, WRITE, and DONE nodes. This is the same visualization available in the workflow editor's Docs → Flow tab, but here it's customer-facing so they can understand what's happening with their data.

**Template cards** — The templates you added in the Layout tab. When a customer clicks one, it launches an Integration Agent session that builds and configures the workflow for them through a chat interface.

> 📷 **Screenshot needed:** Workflows tab showing an active workflow card and template cards below.

> 📷 **Screenshot needed:** Workflow visualizer showing the interactive node graph with labeled node types (TRIGGER → PROCESS → DECISION → WRITE → DONE).

#### Insights tab

Shows performance metrics for the customer's integrations with a time-range selector (7 days / 30 days / 90 days). \[VERIFY: what specific KPI cards appear here? Events processed, sync success rate, active connections, error count?]

> 📷 **Screenshot needed:** Insights tab showing KPI cards with the time-range selector.

***

### Configuring an integration

Click **Configure** on any connected integration in the Apps preview. Two things can happen:

**If no configuration exists yet:** A modal appears prompting "Run the Integration Agent to set up field mappings and filters." Click it and the Integration Agent configures the integration — the same agent experience from [Creating a Workflow via AI](https://claude.ai/fastn/tutorials/saas-admin/creating-a-workflow-via-ai), running directly inside the widget.

**If configuration already exists:** The Integration Configuration dialog opens.

#### Integration Configuration dialog

The dialog header shows the sync type (e.g., "Ongoing sync") and entity count (e.g., "2 entities"). You can toggle between two views:

**Config view** — The working configuration:

Bidirectional sub-tabs let you configure each direction separately (e.g., "HubSpot Company → Cin7 Customer" and "Cin7 Customer → HubSpot Company").

**Field Mappings** show source and target fields as paired pills. Each mapping row displays a preview of real values so you can see what data will actually flow. You can:

* Click **Change** to modify a mapping
* Click the delete icon to remove it
* Click **"Add field mapping"** to add a new one the agent didn't include

**Filters** let you narrow which records sync. Each filter row has a field, an operator, and a value. Available operators: is not empty, is empty, equals, does not equal, contains, greater than, is one of, is not one of.

**Plan view** — Shows the Integration Agent's generated plan for this integration. If empty, it shows: "No plan data available yet. Run the Integration Agent to generate a plan."

The footer reads **"Changes apply on the next workflow run"** with **Cancel** and **Save Configuration** buttons.

> 📷 **Screenshot needed:** Integration Configuration dialog showing bidirectional field mappings with source/target pills and preview values.

> 📷 **Screenshot needed:** Filters section showing 2-3 filter rows with the operator dropdown expanded.

***

### Publishing and sharing

When everything looks right in the preview:

1. Click **Save & Publish** at the bottom-left of the builder panel. The widget is published and available to embed.
2. Use **Reset** to discard unsaved changes if needed.

#### Previewing the widget

A **Preview** button at the top-right of the builder opens a full-page preview of the widget — useful for seeing it without the dashboard around it, or for sharing with your team before embedding.

#### Handing off to your developers

Click the **Embed** tab. It has four sub-tabs: **Iframe**, **MCP**, **SDK**, and **A2A** (the last two are marked "coming soon").

* **Iframe** — The current method for embedding the widget into your product. Shows an **Embed Code** block (with a Copy button) containing the iframe snippet. The widget is embedded via an iframe that loads with a short-lived token your developers generate from your backend. The tab notes: "Live token — expires in 15 min. For production, generate tokens from your backend."
* **MCP** — For exposing your integrations as tools to AI agents (the agent use case). See the Developer tutorials.
* **SDK** (coming soon) — A React component (`@fastn/react`) that will wrap the iframe flow.
* **A2A** (coming soon).

Hand this off to your development team — see [Embedding Fastn](https://claude.ai/fastn/tutorials/developer/embedding-fastn) in the Developer tutorials for the token generation and iframe setup.

> 📷 **Screenshot needed:** Embed tab showing the four sub-tabs (Iframe, MCP, SDK, A2A) with the Iframe tab active and the Embed Code block visible.

***

### What you've learned

* How to open the Widget Builder from the Widgets top nav
* How to add, reorder, and manage integrations in the hub
* How to configure Layout — header content, workflow templates, and Widget Sections (drag to reorder, toggle to show/hide)
* How to customize Style across four sub-tabs: Colors (palette), Typography (fonts), Shape (radii + shadow), and Json (export CSS variables, import tokens)
* How the AI Assistant widget section lets your customers build automations directly inside the widget
* How the three preview tabs work: Apps (connections + AI assistant), Workflows (active + templates + visualizer), Insights (metrics)
* How the Integration Agent handles configuration when a customer clicks Configure for the first time
* How to set up bidirectional field mappings and filters in the Configuration dialog
* How to publish, preview the widget, and hand off the Embed tab to your developers


# Managing customers

Create customers under Settings → Customers, scope API calls per customer, and monitor customer-level activity.

**Prerequisites:** [Your First Integration](https://github.com/fastn-ai/docs/tree/main/getting-started/your-first-integration.md).

In Fastn, your end users are called **Customers** (alternatively called "Tenants"). Each customer gets an isolated environment with their own connections, data, and execution history.

### Creating customers

1. Go to **Settings → Customers**.
2. Click **"Create Customer"**.
3. Enter the customer details (name, and any other required fields).
4. Click create. Note the **customer ID** — you'll use this for API calls and widget embedding.

> **Screenshot:** Settings → Customers showing the Create Customer button and the creation form.

> **Screenshot:** Customer list showing created customers with their IDs.

#### Naming conventions

Customer IDs are used in API headers and widget configuration. Pick a convention early:

* Internal customer ID: `customer_12345`
* Slug: `acme-corp`
* Auto-generated UUID

{% hint style="info" %}
Changing IDs later means updating every API call.
{% endhint %}

### Customer isolation

Every entity is scoped to a customer:

```
Your Organization
├── Customer A (Acme Corp)
│   ├── Connections: Acme's Shopify token, Acme's Slack token
│   ├── Executions: Only Acme's workflow runs
│   └── Data: Only Acme's orders, contacts, invoices
├── Customer B (Widget Inc)
│   ├── Connections: Widget's HubSpot token
│   ├── Executions: Only Widget's workflow runs
│   └── Data: Only Widget's contacts
```

Workflows are defined once by you but execute per-customer using each customer's own connections and data.

### Customizing customer limits

By default, all customers share your plan's quota limits. To override limits for specific customers:

1. Go to **Settings → Billing**.
2. Click **"Customize Customer Limits"**.
3. Set per-customer overrides for any quota dimension.

Use this for:

* High-volume customers who need more events per day
* Trial customers with lower limits
* Enterprise customers with custom agreements

> **Screenshot:** Customize Customer Limits interface showing per-customer quota overrides.

### Monitoring customer activity

#### Activity → Executions

1. Go to **Activity → Executions**.
2. Use the **"All workflows"** dropdown to filter.
3. Each execution row shows **Triggered By** which tells you what started the execution (agent-service, webhook, scheduler, manual).

> **Screenshot:** Activity → Executions filtered to show a specific customer's workflow runs.

#### Activity → Events

1. Go to **Activity → Events**.
2. Filter by type: **All | Webhook | Scheduled | Manual**.
3. See incoming and outgoing events for your customers.

#### Audit Log

For a complete history of all actions:

1. Go to **Settings → Audit Log**.
2. Filter by user, action type, resource, and date range.
3. Columns: Timestamp, User, Action, Resource, Outcome.

### What you've learned

* How to create customers under Settings → Customers
* Customer isolation model i.e. each customer has separate connections, data, and executions
* How to customize quota limits per customer
* How to monitor customer activity through Executions, Events, and Audit Log


# Roles and permissions setup

Assign system roles (Owner, Admin, Developer, Operator, Viewer, End User), create custom roles, and manage team access.

### The 6 system roles

Fastn has 6 system roles. View them under **Settings → ADVANCED → Roles**:

| Role          | Permissions | Description                                                      |
| ------------- | ----------- | ---------------------------------------------------------------- |
| **Owner**     | 39          | Full access within organization and customers                    |
| **Admin**     | 39          | Full access within organization and customers                    |
| **Developer** | 34          | Build connectors, workflows, agents. No billing or org settings. |
| **Operator**  | 18          | Operational access — run workflows, monitor activity             |
| **Viewer**    | 7           | Read-only access                                                 |
| **End User**  | 9           | Customer-facing widget access                                    |

> **Screenshot:** Settings → ADVANCED → Roles showing the 6 system roles with permission counts.

### Permission categories

Each role has granular permissions across 5 categories:

| Category            | Available actions                                               |
| ------------------- | --------------------------------------------------------------- |
| **Connectors** (6)  | create, read, update, delete, deploy test, deploy prod          |
| **Connections** (6) | create, read, update, delete, execute, share                    |
| **Workflows** (7)   | create, read, update, delete, execute, deploy test, deploy prod |
| **Agents** (5)      | create, read, update, delete, execute                           |
| **Tools** (5)       | create, read, update, delete, execute                           |

#### What each role gets

**Owner / Admin** — All 39 permissions. Full access to everything.

**Developer** — 34 permissions. Can build and deploy connectors, workflows, and agents. Cannot manage billing, organization settings, or user roles.

**Operator** — 18 permissions. Can run workflows, monitor executions, and manage connections. Cannot create new connectors or workflows.

**Viewer** — 7 permissions. Read-only across all categories. Can view connectors, workflows, and executions but cannot modify or run anything.

**End User** — 9 permissions. Customer-facing access through the widget. Can use connections and view their own data.

> **Screenshot:** Role detail view for "Admin" showing the permission categories with checkboxes — Connectors 6/6, Connections 6/6, Workflows 7/7, Agents 5/5, Tools 5/5.

### Assigning roles

1. Go to **Settings → People**.
2. Find the team member in the user table.
3. Click the three-dot menu (⋮) on their row.
4. Select their role from the options.

The People table shows: USER, ROLE (badge), TEAMS, STATUS, LAST ACTIVE.

Filter by role, status, or team using the dropdown filters at the top.

> **Screenshot:** Settings → People showing the role badge on a user and the filter dropdowns.

### Creating custom roles

If the system roles don't fit your needs, create a custom role:

1. Go to **Settings → ADVANCED → Roles**.
2. Click **"Create Custom Role"**.
3. Name the role and select which permissions to grant.
4. Save.

Alternatively, duplicate an existing system role and modify it:

1. Click on a system role (e.g., Developer).
2. Click **"Duplicate as Custom"**.
3. Adjust the permissions as needed.
4. Save.

> **Screenshot:** Create Custom Role form or the "Duplicate as Custom" button on a system role.

### Teams

The People section supports **Teams i.e.** groups of users that can be assigned collectively. The user table has a TEAMS column and the filter bar includes an "All Teams" dropdown.

### Audit trail

Every permission-gated action is logged:

1. Go to **Settings → Audit Log**.
2. Filter by: All Users, All Actions, All Types, date range.
3. Columns: **Timestamp, User, Action, Resource, Outcome**.
4. Actions include: `auth.login`, `credential.token_refresh`, and others.
5. Outcomes: **Success** (green badge), **Failure**, **Denied**.

> **Screenshot:** Audit Log showing entries with timestamp, user, action, resource, and outcome columns.

### What you've learned

* The 6 system roles and their permission levels
* 5 permission categories with granular actions
* How to assign roles to team members
* How to create custom roles or duplicate system roles
* Teams support for group management
* Audit log for tracking all actions


# Developer

Tutorials for building on Fastn's platform from local dev setup, custom connectors, TypeScript workflows, deployment, AI agents, to MCP integration.

This section is dedicated for developers embedding Fastn into their own product. Your customers use the embedded widget to connect their apps and configure integrations; your job is to render that widget inside your application and generate the tokens that scope it to each customer.

You don't run Fastn locally or build the integration infrastructure yourself. Fastn runs as a hosted platform. You embed its widget as an iframe, generate a short-lived token from your backend, and decide how connections are scoped to your customers.

### In this section

[**How Embedding Works**](/tutorials/developer/how-embedding-works) — The embedding model: how the iframe, the token, and your backend fit together.

[**Embedding Quickstart**](/tutorials/developer/embedding-quickstart) — Generate a token, render the iframe, and see a working connection.

[**Generating Embed Tokens**](/tutorials/developer/generating-embed-tokens) — The token endpoint, the server-side pattern, and token lifecycle.

[**Authentication & API Keys**](/tutorials/developer/authentication-and-api-keys) — Test vs live keys and the headers each requires.

[**Finding Your Org Identifier**](/tutorials/developer/finding-your-org-identifier) — Locating the `endOrgId` your token and iframe calls need.

[**Understanding Tenancy**](/tutorials/developer/understanding-tenancy) — The two ways connections can be scoped, and how to choose.

[**Deployment**](/tutorials/developer/deployment) — Environment variables and deploy-time considerations.

[**MCP Gateway Integration**](/tutorials/developer/mcp-gateway-integration) — Exposing integrations as tools for AI agents.

[**Troubleshooting**](/tutorials/developer/troubleshooting) — Common errors and their causes.


# How embedding works

Fastn is embedded into your product as an iframe. Your application renders the Fastn widget, and your customers use it to connect their apps and configure integrations without leaving your product.

### How the pieces fit together

There are three parts:

**Your backend** generates a short-lived embed token by calling the Fastn token endpoint with your API key. The token scopes the widget to a specific customer.

**Your frontend** renders the Fastn iframe, passing the token in the URL. The iframe loads the widget your customers interact with.

**The Fastn platform** validates the token, serves the widget, and handles the integration work — authentication with third-party apps, data sync, and workflow execution.

```
Your backend ──(API key)──> Fastn token endpoint ──> short-lived token
     │
     └──> token ──> Your frontend ──> Fastn iframe (with token) ──> Widget
```

### The iframe is the current integration path

The Embed tab in the Widget Builder lists a React SDK, but it is marked "coming soon." Today, the supported method is the iframe. This guide documents the iframe path.

When the React SDK ships, it will wrap this same flow — your backend will still generate the token, and the SDK will render the iframe for you. The token generation pattern you build now will carry over.

### What you'll set up

1. A server-side function that generates an embed token on each request (never hardcode tokens — they expire in 15 minutes).
2. A frontend component that renders the Fastn iframe with the token.
3. A decision about how connections are scoped — shared across the customer's organization, or isolated per individual user (see [Tenancy](https://claude.ai/fastn/tutorials/developer/tenancy)).

### Why generate tokens server-side

The token is minted from your API key. Your API key must never be exposed in client-side code. The token also expires after 15 minutes, so it can't be generated once and reused — each page load needs a fresh token. Both of these require a backend step.

A common failure is hardcoding a token into the frontend during testing. It works for the first 15 minutes, then the iframe renders blank with no obvious error. Generating the token server-side per request avoids this entirely.

### Next

Start with the [Quickstart](https://claude.ai/fastn/tutorials/developer/quickstart) to get a working embed, then read [Generating Embed Tokens](https://claude.ai/fastn/tutorials/developer/generating-embed-tokens) for the full token details.


# Embedding quickstart

This walks through the shortest path to a working embed: generate a token from your backend, render the iframe, and see a connection. It assumes you have a published widget and an API key.

### Before you start

You'll need:

* **An API key** — From Settings → API Keys. Use a `fsk_test_` key while developing. See [Authentication & API Keys](https://claude.ai/fastn/tutorials/developer/authentication-and-api-keys).
* **Your org identifier (`endOrgId`)** — The internal UUID for the customer the widget is for. See [Finding Your Org Identifier](https://claude.ai/fastn/tutorials/developer/finding-your-org-identifier).
* **A backend** — Any server environment where you can make an HTTP request with a secret API key.

### Step 1: Generate a token from your backend

Create a server-side function that calls the Fastn token endpoint. Keep the API key in an environment variable — never in client code.

```javascript
// Server-side only
async function getFastnToken({ userEmail, userName }) {
  const res = await fetch("https://live.gcp.fastn.ai/api/v1/embed/token", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.FASTN_API_KEY}`,
      "Content-Type": "application/json",
      // Required only for fsk_test_ keys:
      "X-fastn-Test-Mode": "true",
    },
    body: JSON.stringify({
      endOrgId: process.env.FASTN_END_ORG_ID,
      userEmail,
      userName,
    }),
  });

  const json = await res.json();
  // The token is nested under `data`
  return json.data.token;
}
```

> Note: the response nests the token under `data` — read `json.data.token`, not `json.token`.

### Step 2: Expose the token to your frontend

Create an endpoint your frontend can call to get a fresh token. Return only the token, never the API key.

```javascript
// Example route
app.get("/api/fastn-token", async (req, res) => {
  const token = await getFastnToken({
    userEmail: req.user.email,
    userName: req.user.name,
  });
  res.json({ token });
});
```

### Step 3: Render the iframe

On your frontend, fetch a token and render the Fastn iframe with it.

```javascript
const { token } = await fetch("/api/fastn-token").then(r => r.json());

const iframe = document.createElement("iframe");
// Org-level (shared) embed — see the Tenancy guide for user-level
iframe.src = `https://live.gcp.fastn.ai/api/v1/embed/iframe?token=${token}`;
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.border = "none";
document.getElementById("integrations").appendChild(iframe);
```

### Step 4: Verify

Load the page. The Fastn widget should render inside your application. You should see the integrations you added to the widget, each with a Connect button.

Click Connect on one, complete the authentication popup, and confirm the app shows as Connected.

### If the iframe is blank

The most common cause is an expired or hardcoded token. Confirm your frontend fetches a fresh token on each load rather than reusing one. See [Troubleshooting](https://claude.ai/fastn/tutorials/developer/troubleshooting).


# Generating embed tokens

Embed tokens are short-lived credentials that scope the Fastn widget to a specific customer. They are generated from your backend using your API key and passed to the iframe.

### The token endpoint

```
POST https://live.gcp.fastn.ai/api/v1/embed/token
```

**Headers:**

```
Authorization: Bearer <FASTN_API_KEY>
Content-Type: application/json
X-fastn-Test-Mode: true        (only for fsk_test_ keys; omit for fsk_live_)
```

**Body:**

```json
{
  "endOrgId": "<customer-org-uuid>",
  "userEmail": "user@example.com",
  "userName": "User Name"
}
```

| Field       | Description                                                                                                                                                                                                                   |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `endOrgId`  | The internal UUID of the customer the widget is for. Must be the UUID — the slug and External ID both return 404. See [Finding Your Org Identifier](https://claude.ai/fastn/tutorials/developer/finding-your-org-identifier). |
| `userEmail` | The email of the individual user. Used for user-level tenancy (see [Tenancy](https://claude.ai/fastn/tutorials/developer/tenancy)).                                                                                           |
| `userName`  | The display name of the individual user.                                                                                                                                                                                      |

**Response:**

```json
{
  "data": {
    "token": "emb_...",
    "endOrgId": "<customer-org-uuid>",
    "expiresIn": 900
  }
}
```

The embed token is prefixed `emb_`. In the hands-on integration, the token was nested under `data` — read `response.data.token`, not `response.token`.

`expiresIn` is `900` seconds — 15 minutes.

> **VERIFY:** The Embed tab UI shows the token request with only `Authorization: Bearer YOUR_API_KEY` and references a `YOUR_CUSTOMER_ID` parameter, while the hands-on integration used a body of `{ endOrgId, userEmail, userName }` and read the token from `data.token`. Confirm the exact request body and response shape against the current API before publishing — the UI snippet and the hands-on result should be reconciled.

### Refreshing tokens

Because tokens expire after 15 minutes, a long-lived embed session needs the token refreshed before it expires. The platform provides a refresh endpoint, and the Embed tab notes that tokens auto-refresh:

```
POST https://live.gcp.fastn.ai/api/v1/embed/token/refresh
```

Use the refresh endpoint to obtain a new token before the current one expires, or generate a fresh token from your backend on each load for shorter sessions.

> **VERIFY:** The refresh endpoint exists (confirmed on the platform). Confirm its exact request and response shape, and whether the SDK (when it ships) handles auto-refresh automatically.

### Token lifecycle

**Tokens expire after 15 minutes.** Generate a fresh token on each page load. Do not generate one and reuse it across sessions or store it for later.

**Generate tokens server-side.** The token is minted from your API key, and your API key must never appear in client-side code. The token request has to happen on your backend.

**Never hardcode a token.** A token pasted into frontend code works until it expires, then the iframe renders blank with no clear error. This is the most common embedding mistake. Always fetch a fresh token from your backend at render time.

### The recommended pattern

Put token generation in a server-side function that reads the API key and org ID from environment variables and returns a fresh token per request:

```javascript
async function getFastnToken({ userEmail, userName }) {
  const res = await fetch("https://live.gcp.fastn.ai/api/v1/embed/token", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.FASTN_API_KEY}`,
      "Content-Type": "application/json",
      "X-fastn-Test-Mode": "true", // omit for live keys
    },
    body: JSON.stringify({
      endOrgId: process.env.FASTN_END_ORG_ID,
      userEmail,
      userName,
    }),
  });

  if (!res.ok) {
    throw new Error(`Token request failed: ${res.status}`);
  }

  const json = await res.json();
  return json.data.token;
}
```

Expose this through an endpoint your frontend calls at render time. Return only the token.

This pattern is what makes the embed reliable across different users and sessions. Hardcoding or caching tokens is what causes "works for me, fails for them" behavior.


# Authentication and API keys

Fastn API keys authenticate your backend's requests — including generating embed tokens. Keys come in test and live variants, and each requires different handling.

### Where to find your keys

Go to **Settings → API Keys**. The page has a **Test / Live toggle** in the top-right and a keys table with a **MODE** column — each key shows a badge indicating whether it's a LIVE or TEST key. Click **+ Create API Key** to generate one. Keys are shown once at creation — copy and store it securely.

When viewing test data, the page shows a banner: "Test keys only work with the X-fastn-Test-Mode: true header."

### Test vs live keys

Keys are in the `fsk_` format, with two variants:

| Variant | Prefix      | Use for                 |
| ------- | ----------- | ----------------------- |
| Test    | `fsk_test_` | Development and testing |
| Live    | `fsk_live_` | Production              |

#### Test keys require an extra header

Requests made with a `fsk_test_` key **must** include this header:

```
X-fastn-Test-Mode: true
```

Without it, requests with a test key fail in ways that aren't obvious from the error. If you're getting unexpected failures with a test key, this header is the first thing to check.

#### Live keys must not send that header

Requests with a `fsk_live_` key must **not** include `X-fastn-Test-Mode`. Send the header only with test keys.

### Example

Test key:

```javascript
headers: {
  "Authorization": "Bearer fsk_test_...",
  "Content-Type": "application/json",
  "X-fastn-Test-Mode": "true",   // required
}
```

Live key:

```javascript
headers: {
  "Authorization": "Bearer fsk_live_...",
  "Content-Type": "application/json",
  // no X-fastn-Test-Mode header
}
```

### Keeping keys secure

* Store keys in environment variables, never in source code or client-side bundles.
* Use test keys in development and live keys only in production.
* The API key is used to mint embed tokens on your backend — it should never reach the browser. See [Generating Embed Tokens](https://claude.ai/fastn/tutorials/developer/generating-embed-tokens).


# Finding your org identifier

The `endOrgId` identifies the customer a widget embed is for. Both the token endpoint and the iframe URL require it. Getting the right identifier is necessary for anything to work.

### It must be the customer UUID

`endOrgId` must be the customer's internal UUID. The customer **slug** (e.g., `katana-customer`) does not work in its place.

### Where the UUID comes from

Each customer in your Fastn workspace has an internal UUID. You create customers under **Settings → Customers** (see [Managing Customers](https://claude.ai/fastn/tutorials/saas-admin/managing-customers)).

The customer record in the dashboard shows: Name, Slug, Status, Customer Admin, and Created date. It does **not** display the UUID — there is no UUID or External ID field in the Customers UI.

To retrieve the UUID today, open the customer in the dashboard and read it from the page state using your browser's developer tools.

> **VERIFY:** This is a confirmed gap — the customer UUID is not exposed anywhere in the Customers UI (validated on the live platform). The Embed tab refers to this value as `YOUR_CUSTOMER_ID` / `customer_id`. Confirm the recommended retrieval method, and update this page if the UUID becomes available directly in the UI or via an API endpoint.

### Using it

Once you have the UUID, pass it as `endOrgId` in:

* The token request body (`{ endOrgId, userEmail, userName }`)
* The iframe URL for user-level embeds (`?tenant-id=<endOrgId>&token=<token>`)

Store it in an environment variable rather than hardcoding it, so it can change per environment (test customer vs production customer).

```
FASTN_END_ORG_ID=<customer-uuid>
```


# Understanding tenancy

An overview of how tenancy works per user and org level in Fastn

Fastn supports two ways to scope connections within the same embed: org-level (shared) and user-level (per individual). Choosing correctly determines who can see and use which connections.

### The distinction

**Org-level (shared)** — One connection serves the entire customer organization. Anyone in that organization uses the same connection. Use this for shared systems of record.

**User-level (per individual)** — Each user has their own connection, isolated from other users in the same organization. Use this for resources tied to one person's identity.

{% hint style="info" %}
This is **not** an "internal vs external user" distinction, and it is **not** about who can edit. It is strictly shared-vs-personal, based on who owns the connected resource.
{% endhint %}

### How to decide

Ask who owns the resource being connected:

| Resource type                         | Scope      | Examples                                                           |
| ------------------------------------- | ---------- | ------------------------------------------------------------------ |
| Shared systems of record              | Org-level  | CRM, project management, data warehouse, shared messaging channels |
| Personal identity, inbox, or calendar | User-level | A person's email, personal calendar, individual account            |

A shared Salesforce instance that the whole company uses is org-level. An individual sales rep's email inbox is user-level. Most customers need both in the same embed — a shared CRM connection plus each rep's own email.

### How it's set in the embed

The tenancy choice is encoded in the iframe URL and the identity the token is minted with.

**User-level (per individual):**

```
https://live.gcp.fastn.ai/api/v1/embed/iframe?tenant-id=<endOrgId>&token=<token>
```

The `tenant-id` parameter is present. Combined with the `userEmail` and `userName` in the token body, connections are isolated to that individual user.

**Org-level (shared):**

```
https://live.gcp.fastn.ai/api/v1/embed/iframe?token=<token>
```

No `tenant-id` parameter. Connections are shared across the organization.

### Persona examples

These illustrate the typical mix of shared and personal connections per buyer persona:

| Persona                            | Org-level (shared) | User-level (personal)         |
| ---------------------------------- | ------------------ | ----------------------------- |
| Integration Backlog Owner (VP Eng) | Jira, Slack        | Google Calendar               |
| Product Manager                    | Ad account, CRM    | Slack alerts                  |
| CRO                                | Salesforce         | Each rep's email              |
| Head of Solution Engineering       | Snowflake, CRM     | Each tester's sandbox account |
| Head of Customer Success           | Usage-data system  | Each CSM's calendar / email   |

The through-line: shared systems of record map to org-level; personal identity, inbox, or calendar maps to user-level. Most customers need both, which is why both flavors exist.


# Deployment

When you deploy the application you've embedded Fastn into, the main consideration is how environment variables and secrets bind on your deploy target. Getting this wrong is a common cause of a working local build that fails in production.

### Environment variables: runtime vs build time

Your embedded integration needs at least:

* `FASTN_API_KEY` — your `fsk_` API key
* `FASTN_END_ORG_ID` — the customer UUID

These must be available to the server-side function that generates embed tokens.

On some hosts, environment variables set in a CI pipeline are not automatically available to the deployed runtime. They bind at deploy time on the hosting target, not from the pipeline's own secrets. If the token function reads an undefined API key in production, token generation fails and the embed renders blank.

### Checklist for deploying

1. Set `FASTN_API_KEY` and `FASTN_END_ORG_ID` directly on the hosting target (not only in CI).
2. Use a `fsk_live_` key in production (and drop the `X-fastn-Test-Mode` header — see [Authentication & API Keys](/tutorials/developer/authentication-and-api-keys)).
3. Redeploy after changing environment variables, if your host binds them at deploy time.
4. Paste secret values raw — no surrounding quotes or brackets.
5. Confirm the token endpoint is reachable from your deployed backend.

### Verifying after deploy

Load the embedded page in production and confirm the widget renders and a token is being generated fresh on each load. If the iframe is blank, check that the environment variables are bound on the runtime, not just present in CI. See [Troubleshooting](/tutorials/developer/troubleshooting).


# MCP gateway integration

Connect AI assistants to Fastn's MCP server i.e. native tools, dynamic tools, per-customer scoping, and integration patterns.

**Prerequisites:** Understanding of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). A Fastn project with at least one configured connector.

Fastn implements an MCP server that exposes your integrations as tools for AI assistants. Any MCP-compatible client Claude Desktop, custom agents, or your own AI features can discover and invoke these tools.

### What the MCP gateway provides

#### Native tools

Six built-in tools are always available:

| Tool                           | What it does                                      |
| ------------------------------ | ------------------------------------------------- |
| `fastn_list_integrations`      | List all active integrations for a customer       |
| `fastn_get_integration_status` | Get detailed status of a specific integration     |
| `fastn_create_flow`            | Create a new automation flow from a specification |
| `fastn_get_event_history`      | Retrieve recent events for a customer             |
| `fastn_get_usage_summary`      | Get quota usage summary                           |
| `fastn_search_entities`        | Search CDM entities across integrations           |

#### Dynamic tools

In addition to the native tools, the gateway auto-generates tools from your connector capabilities. If a customer has Slack and Shopify connected, the gateway exposes tools like:

* `slack_send_message`
* `slack_list_channels`
* `shopify_list_orders`
* `shopify_get_customer`

Dynamic tools are scoped per tenant — a customer who only connected Slack will only see Slack tools. A tenant with Slack and Shopify sees both.

### Connecting to the MCP server

#### MCP server URL

Your MCP server URL follows this pattern:

```
https://mcp.ucl.dev/mcp/?id={PROJECT_ID}&api_key={API_KEY}&space_id={PROJECT_ID}
```

#### Claude Desktop configuration

Add Fastn as an MCP server in Claude Desktop's configuration:

```json
{
  "mcpServers": {
    "fastn": {
      "url": "https://mcp.ucl.dev/mcp/?id=YOUR_PROJECT_ID&api_key=YOUR_API_KEY&space_id=YOUR_PROJECT_ID"
    }
  }
}
```

After configuration, Claude can discover and invoke Fastn tools directly in conversation.

> **Screenshot:** Claude Desktop with Fastn MCP tools visible in the tool list and showing both native and dynamic tools.

#### Programmatic integration

For custom AI applications, connect to the MCP server using any MCP client library:

```typescript
// Example using the MCP SDK
import { Client } from '@modelcontextprotocol/sdk/client';

const client = new Client({
  name: 'my-ai-app',
  version: '1.0.0'
});

// Connect to Fastn's MCP server
await client.connect({
  url: 'https://mcp.ucl.dev/mcp/?id=PROJECT_ID&api_key=API_KEY&space_id=PROJECT_ID'
});

// List available tools
const tools = await client.listTools();
console.log(tools);
// Returns: fastn_list_integrations, fastn_search_entities, slack_send_message, ...

// Invoke a tool
const result = await client.callTool({
  name: 'fastn_list_integrations',
  arguments: { tenant_id: 'acme-corp' }
});
```

#### Using with the Anthropic API

If you're building with Claude via the API, pass the MCP server in your request:

```javascript
const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1000,
    messages: [
      { role: "user", content: "List all active integrations for tenant acme-corp" }
    ],
    mcp_servers: [
      {
        type: "url",
        url: "https://mcp.ucl.dev/mcp/?id=PROJECT_ID&api_key=API_KEY&space_id=PROJECT_ID",
        name: "fastn"
      }
    ]
  })
});
```

> **Screenshot:** API response showing Claude using a Fastn MCP tool and returning integration data.

### Per-customer tool scoping

This is what makes Fastn's MCP gateway multi-customer-aware. When an AI assistant invokes a tool, the gateway:

1. Identifies the customer from the request context
2. Checks which connectors the customer has active
3. Only exposes tools for the customer's connected apps
4. Executes the tool using the customer's credentials

Customer A (connected: Slack, Shopify) sees:

```
fastn_list_integrations, fastn_search_entities, ...,
slack_send_message, slack_list_channels,
shopify_list_orders, shopify_get_customer
```

Customer B (connected: Slack only) sees:

```
fastn_list_integrations, fastn_search_entities, ...,
slack_send_message, slack_list_channels
```

No Shopify tools appear for Customer B because they haven't connected Shopify.

### Resource listing

The MCP gateway also implements the MCP resource protocol, allowing AI assistants to discover available data:

```typescript
// List available resources
const resources = await client.listResources();
// Returns: integrations, entities, events, usage data
```

Resources provide read access to data without executing actions — useful for AI assistants that need to answer questions about a customer's integration state.

### Integration patterns

#### Pattern 1: AI-powered support

Your support team uses Claude Desktop with Fastn MCP tools. When a customer reports an issue, the agent can:

1. Look up the customer's integrations (`fastn_list_integrations`)
2. Check integration status (`fastn_get_integration_status`)
3. Search for related entities (`fastn_search_entities`)
4. View recent events (`fastn_get_event_history`)

#### Pattern 2: In-product AI assistant

Your SaaS product has an AI chat feature. You connect it to Fastn via MCP so users can:

* "Show me my recent Shopify orders" → `shopify_list_orders`
* "Send a summary to my Slack channel" → `slack_send_message`
* "What integrations do I have active?" → `fastn_list_integrations`

#### Pattern 3: Automated operations

A monitoring agent runs on a schedule, uses MCP tools to check integration health, and creates alerts when something fails:

* Check all customers' integration status
* Query event history for failures
* Review usage against quotas
* Create notifications for the ops team

### What you've learned

* The 6 native MCP tools and how dynamic tools are generated from connectors
* How to connect Claude Desktop, custom apps, and the Anthropic API to Fastn's MCP server
* How per-customer tool scoping works
* Three integration patterns for real-world use


# Troubleshooting

Common errors when embedding Fastn, with their causes and fixes.

### Blank embed / iframe renders nothing

**Most likely cause:** an expired or hardcoded token.

Embed tokens expire after 15 minutes. If a token is hardcoded into your frontend or cached and reused, it works until it expires, then the iframe loads blank with no obvious error.

**Fix:** Generate a fresh token from your backend on each page load. Never hardcode or cache tokens. See [Generating Embed Tokens](/tutorials/developer/generating-embed-tokens).

**Also check:** In production, confirm `FASTN_API_KEY` is bound on the runtime, not just present in CI. An undefined API key means token generation fails silently. See [Deployment](/tutorials/developer/deployment).

### 404 on the token request

**Most likely cause:** the wrong org identifier.

`endOrgId` must be the customer's internal UUID. The customer slug and the External ID both return 404.

### Requests fail with a test key

**Most likely cause:** the missing test-mode header.

Requests made with a `fsk_test_` key require the header `X-fastn-Test-Mode: true`. Without it, requests fail in ways the error message doesn't make obvious.

**Fix:** Add `X-fastn-Test-Mode: true` for test keys. Remove it for live keys. See [Authentication & API Keys](https://claude.ai/fastn/tutorials/developer/authentication-and-api-keys).

### "Failed to fetch" in the widget

**Most likely cause:** a tenancy or token mismatch.

This can appear when the iframe URL's tenancy shape doesn't match how the token was minted — for example, a single-tenant embed where the `tenant-id` and token identity don't line up.

**Fix:** Confirm the iframe URL matches your intended tenancy (see [Tenancy](/tutorials/developer/understanding-tenancy)), and that the token was generated with the correct `endOrgId` and identity fields. Confirm the backend token pattern is being used rather than a hardcoded token.

### Empty response token

**Most likely cause:** reading the token from the wrong place in the response.

The token is nested under `data` in the response — `response.data.token`, not `response.token`.

**Fix:** Read `json.data.token`. See [Generating Embed Tokens](/tutorials/developer/generating-embed-tokens).


# End User

Tutorials for end users interacting with Fastn through the embedded widget — connecting apps, customizing integrations, and viewing sync status.

These tutorials are for the people who use integrations through the widget embedded in a SaaS product. You don't need a Fastn account or dashboard access — everything happens inside the product you already use.

{% hint style="info" %}
**Note for SaaS Admins:** Share this section with your customers when they ask how to use the integrations you've embedded. These tutorials describe the end user experience through the widget, not the Fastn dashboard.
{% endhint %}

### Tutorials in this section

[**Connecting Apps via Widget**](/tutorials/end-user/connecting-apps-via-widget) — Find available integrations, authorize your accounts, and activate connections through the embedded widget.

[**Customizing Your Integrations**](/tutorials/end-user/customizing-your-integrations) — Configure sync preferences, field mappings, scheduling, and other settings for your active integrations.

[**Viewing Sync Status & History** ](/tutorials/end-user/viewing-sync-status-and-history)— Check what's syncing, view execution history, and troubleshoot when something fails.


# Connecting apps via the widget

Find available integrations, authorize your accounts, and activate connections through the embedded integration widget.

The product you use has an integrations section powered by Fastn. This is where you connect your third-party apps your Slack workspace, your Shopify store, your HubSpot account so data can flow between them and the product you're using.

**Prerequisites:** Access to a SaaS product that uses Fastn for integrations. An account on the third-party app you want to connect (e.g., Slack, Shopify, HubSpot).

### Finding the integration widget

The integration widget is embedded somewhere in the product you use. Common locations:

* A dedicated "Integrations" page or tab
* A settings section labeled "Connected Apps" or "Connections"
* A sidebar menu item

The exact location depends on how the SaaS company set things up. If you can't find it, check the product's help docs or ask their support team.

> **Screenshot:** Example of an embedded widget as it appears inside a SaaS product — showing the integration hub with available app icons and connect buttons.

### Navigating the widget

The widget has three tabs at the top:

* **Apps** — Connect and manage your third-party apps
* **Workflows** — View active automations and available templates
* **Insights** — Monitor sync activity and performance

{% hint style="warning" %}
Widget options and fucntionalities may vary per SaaS company's settings and configurations.
{% endhint %}

At the top of the Apps tab, a **search bar** lets you filter available integrations by name or category and useful when many apps are available.

### Connecting an app

1. Open the integration widget in your product.
2. You'll see a list of available integrations
3. Find the app you want to connect by scrolling or using the search bar.
4. Click **Connect** (the button label may vary).

> **Screenshot needed:** Widget showing a list of available integrations with Connect buttons next to each one, and the search bar at the top.

5. A popup opens with the third-party app's authorization screen. This is the app's own login page not the product's, not Fastn's.
6. Sign in with your account on the third-party app.
7. Review the permissions being requested and click **Authorize** (or "Allow").
8. The popup closes and you're redirected back to the widget. The app now shows a green dot with a **"Connected"** label beneath the app name.

### What happens during authorization

When you click Connect and authorize, you're giving the product permission to access your data on the third-party app. For example:

* Connecting Slack lets the product send messages to your channels or read your channel list
* Connecting Shopify lets the product read your orders, products, or customers
* Connecting HubSpot lets the product read or create contacts in your account

The product can only do what it was set up to do — it can't access anything beyond the specific actions configured by the SaaS company.

Your credentials are stored securely and are isolated from other users. No one else using the same product can see your connections or access your third-party accounts.

### Connecting multiple apps

Some products let you connect multiple apps at once. Each connection is independent and connecting Slack doesn't affect your Shopify connection, and disconnecting one doesn't disconnect others.

If the product supports it, you may also be able to connect multiple accounts of the same app (e.g., two different Slack workspaces). Look for a "Add another connection" option.

### Disconnecting an app

If you want to stop an integration:

1. Open the widget.
2. Find the connected app.
3. Click **Disconnect** (or "Deactivate" / "Remove").
4. The connection is removed. The product can no longer access your data on that app.

Disconnecting removes the stored credentials. If you reconnect later, you'll need to authorize again.

> **Screenshot:** Widget showing a connected app with the Disconnect button visible.

### Troubleshooting

**The Connect button does nothing:** Check if your browser is blocking popups. The authorization screen opens in a popup window, if it's blocked, nothing happens. Allow popups for this site and try again.

**Authorization fails:** Make sure you're signing into the correct account on the third-party app. If you have multiple accounts (e.g., multiple Slack workspaces), verify you're authorizing the right one.

**The app shows "Error" after connecting:** The authorization may have succeeded but the initial setup failed. Try disconnecting and reconnecting. If the error persists, contact the product's support team.

### What you've done

* Found the integration widget in your product
* Connected a third-party app by authorizing your account
* Understand what permissions are granted and how your data is protected


# Customizing your integrations

Configure sync preferences, field mappings, scheduling, and other settings for your active integrations through the widget.

After connecting an app, you can customize how data flows between it and the product you're using which fields sync, which records are included, and in which direction. The AI assistant can handle most of this for you, or you can fine-tune it yourself.

### Opening the configuration

1. Open the integration widget in your product.
2. Find the connected app you want to customize.
3. Click **Configure**.

What happens next depends on whether the integration has been configured before.

#### First-time configuration

If no configuration exists yet, a modal appears prompting you to **run the Integration Agent**. The AI agent sets up field mappings and filters for you automatically, it figures out which fields in one app correspond to fields in the other, and configures sensible defaults.

Click through the prompt and the agent handles the setup. Once it finishes, the configuration dialog opens with everything pre-configured. You can review what the agent did and adjust anything that doesn't look right.

> **Screenshot needed:** Modal prompting "Run the Integration Agent to set up field mappings and filters" with the action button.

#### Returning to an existing configuration

If the integration has already been configured (either by the AI or by you previously), clicking Configure opens the **Integration Configuration dialog** directly.

> **Screenshot needed:** A connected app card (e.g., HubSpot with green "Connected" label) with the ⚙ Configure button highlighted.

### The configuration dialog

The dialog header shows the sync type (e.g., "Ongoing sync") and how many data types are configured (e.g., "2 entities"). Below the header, you can switch between two views: **Config** and **Plan**.

#### Config view

This is where you review and adjust how data flows.

**Sync direction tabs**

If the integration syncs data in both directions, you'll see sub-tabs for each direction, for example:

* **"HubSpot Company → Cin7 Customer"** (data flowing from HubSpot to Cin7)
* **"Cin7 Customer → HubSpot Company"** (data flowing from Cin7 to HubSpot)

Click a tab to see and edit the field mappings for that direction. Each direction has its own independent configuration.

> **Screenshot needed:** Configuration dialog showing the bidirectional sub-tabs at the top, with one direction active.

**Field mappings**

Field mappings control which fields in one app correspond to fields in the other. Each mapping row shows:

* A plain-language description of what it does
* The **source field** (where the data comes from) and the **target field** (where it goes) displayed as paired labels
* A preview of actual values so you can see what data will flow
* A **Change** button to modify the mapping
* A delete icon to remove it

To add a mapping the AI didn't include, click **"Add field mapping"** at the bottom of the list.

> **Screenshot needed:** Field mappings section showing 3-4 mapping rows with source/target labels, preview values, and Change buttons.

**Filters**

Filters let you narrow which records sync. For example, you might only want to sync contacts tagged as "Active" or orders above a certain amount.

Each filter row has three parts:

* **Field** — Which field to check (e.g., "Status," "Amount," "Email")
* **Operator** — The condition to apply
* **Value** — What to compare against

Available operators:

| Operator       | What it does                                                    |
| -------------- | --------------------------------------------------------------- |
| is not empty   | Syncs records where the field has any value                     |
| is empty       | Syncs records where the field is blank                          |
| equals         | Syncs records where the field matches exactly                   |
| does not equal | Syncs records where the field doesn't match                     |
| contains       | Syncs records where the field includes the text                 |
| greater than   | Syncs records where the field is above a number                 |
| is one of      | Syncs records where the field matches any value in a list       |
| is not one of  | Syncs records where the field doesn't match any value in a list |

> **Screenshot needed:** Filters section showing 2-3 filter rows with the operator dropdown expanded.

### Saving your changes

After making adjustments:

1. Click **Save Configuration** at the bottom of the dialog.
2. The footer confirms: **"Changes apply on the next workflow run."**

This means your changes don't take effect immediately on existing data. The next time the automation runs (whether that's in real-time, on a schedule, or manually triggered), it uses your updated configuration. Existing data that has already synced is not retroactively changed and only new or updated records use the new settings.

Click **Cancel** to discard your changes and close the dialog without saving.

### Using the AI assistant for changes

If you're not sure how to adjust the configuration manually, use the AI assistant at the bottom of the widget's Apps tab. Describe what you want in plain language:

* "Only sync contacts with an email address"
* "Stop syncing orders under $50"
* "Add the phone number field to the sync"

Click **Build with AI** and the agent will update the configuration for you.

> **Screenshot needed:** AI assistant input at the bottom of the widget with an example prompt like "Only sync contacts with an email address."

### Viewing active workflows

Click the **Workflows** tab in the widget to see the automations running on your integrations. Active workflows show as cards, click the arrow on any workflow to open an interactive visual diagram showing how the automation works, step by step. You can trace the flow from trigger to completion and see each decision point along the way.

You'll also see **template cards** for pre-built automations. Click one to launch an AI assistant session that sets it up for you.

> **Screenshot needed:** Workflows tab showing an active workflow card and a template card.

> **Screenshot needed:** Workflow visualizer showing the interactive node graph with labeled steps.

### Troubleshooting

* **I don't see a Configure button**\
  Not all integrations support customization. The SaaS company decides what's configurable. If you need changes that aren't available, contact the product's support team.
* **My changes didn't take effect:**\
  Changes apply on the next workflow run, not immediately. If the integration runs daily, your changes will be reflected in the next day's sync. Check the Insights tab to confirm when the next run happens.
* **I want to start over:**\
  Disconnect the app from the Apps tab and reconnect it. This clears the configuration. When you click Configure again, the Integration Agent will set up a fresh configuration.

### What you've done

* Opened the configuration dialog for a connected integration
* Understood the two paths: AI-assisted first-time setup vs manual adjustment
* Reviewed and modified field mappings and filters
* Know how to use the AI assistant for configuration changes
* Know that changes apply on the next workflow run


# Viewing sync status and history

Check what's syncing, view execution history, spot errors, and know when to contact support.

Once your integrations are connected and running, the widget shows you how they're doing, what's running, what's succeeded or failed, and what needs attention. This information lives mainly on the **Insights** tab, with per-workflow run history available on the **Workflows** tab.

**Prerequisites:** At least one app connected via the widget. See [Connecting Apps via Widget](https://claude.ai/fastn/tutorials/end-user/connecting-apps-via-widget).

***

### The Insights tab

Open the **Insights** tab in the widget. This is where most of your sync and health information lives. A time-range selector in the top-right lets you switch between **7 days**, **30 days**, and **90 days** (7 days is selected by default).

#### Summary cards

Three cards across the top give you an at-a-glance view:

| Card                  | Shows                                                                              |
| --------------------- | ---------------------------------------------------------------------------------- |
| **Runs Today**        | Total runs today, broken into successful and failed (e.g., "1.2k ok · 251 failed") |
| **Records Processed** | How many records moved, split into synced, failed, and skipped                     |
| **Needs Attention**   | How many workflows need action                                                     |

#### Workflows table

Below the cards, a table lists your workflows with these columns:

| Column   | Shows                                                                  |
| -------- | ---------------------------------------------------------------------- |
| NAME     | The workflow name                                                      |
| LAST RUN | When it last ran, as a relative time (e.g., "40m ago", "2d ago")       |
| STATUS   | The latest status (e.g., "failed", or blank if the last run was clean) |
| RUNS     | Total number of runs                                                   |
| AVG      | Average run duration (e.g., "20.2s")                                   |

A footer summarizes activity, such as "5/13 active" and "8 not run in 30d".

#### Connectors

A **Connectors** section shows summary tiles ("Connected" and "Broken" counts) and a per-connector row with a status dot and a short status — for example, "Slack" with "no sync yet" if it hasn't run a sync yet.

#### Needs Attention

A **Needs Attention** list surfaces recent errors. Each entry shows:

* A category badge (CONNECTION, CONFIGURATION, USER\_DATA, or DATA)
* A short error summary
* The workflow it came from
* A relative timestamp

This is the fastest place to see what's gone wrong and which integration it affects.

***

### Per-workflow history (Workflows tab)

The **Workflows** tab lists your workflows as cards. Each card shows the workflow's title and a short description of what it does and when it runs. The card itself doesn't show run times or status — those are on the Insights tab and in the detail view.

Each card has a run button, a remove button, and a **chevron** that opens a detail view with four tabs:

| Tab            | Shows                                                                                                                  |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Diagram**    | A visual node-graph of the workflow's logic                                                                            |
| **Executions** | Run history — each row has a status badge (COMPLETED or FAILED), an execution ID, a duration, and a relative timestamp |
| **Errors**     | Errors with a count badge — each shows an error-type badge (e.g., CONNECTION), the message, and a timestamp            |
| **Test Cases** | The test scenarios for the workflow, with a count badge                                                                |

The **Executions** tab is where you see the full run-by-run history for a single workflow.

***

### Where to look for what

| You want to know\...                             | Look at...                               |
| ------------------------------------------------ | ---------------------------------------- |
| Overall health today (runs, successes, failures) | Insights → summary cards                 |
| Which workflows ran and when                     | Insights → workflows table               |
| What's currently failing or needs action         | Insights → Needs Attention               |
| Whether a connector is connected or broken       | Insights → Connectors                    |
| The full run history of one workflow             | Workflows tab → open a card → Executions |
| Why a specific workflow failed                   | Workflows tab → open a card → Errors     |
| What a workflow does, visually                   | Workflows tab → open a card → Diagram    |

***

### Reading timestamps

Run times appear as relative timestamps ("40m ago", "14h ago", "2d ago") rather than absolute dates. "Last run" on the Insights table and the timestamps in the Executions list both use this format.

***

### A connected app's status

On the **Apps** tab, a connected integration shows its connection state — the name, a status dot, and a connection count (e.g., "1/2 connected") — but not sync activity or errors. For sync status, go to the **Insights** tab, where the Connectors section shows each connector's sync state.

***

### What you've learned

* How to read overall sync health from the Insights summary cards
* How to find which workflows ran, when, and how often
* How to see what needs attention and why, including error categories
* How to open a workflow's full run history, errors, and diagram from the Workflows tab
* Where connector sync state is shown (Insights, not the Apps tab)


# Build

Everything that defines how an integration behaves.

Build is where integrations are made. Nine pages, in roughly the order you meet them.

Six of them match a screen under **Integrations** in the sidebar. The **Agent** is reached from the Home *What do you want to build?* prompt rather than the sidebar, and *MCP gateway* and *Common patterns* have no screen of their own — they cut across several. The other BUILD nav item, **Widgets**, is documented under [Embed](/embed/embed).

{% content-ref url="/pages/OKnqjJeOLRLCVQ7rOqK0" %}
[Agent](/build/agent)
{% endcontent-ref %}

{% content-ref url="/pages/j1VdyWVKKtH0vXVJHDwZ" %}
[Connectors](/build/connectors)
{% endcontent-ref %}

{% content-ref url="/pages/333TnWt6GTuwmpDt9G6Q" %}
[Unified APIs](/build/unified-apis)
{% endcontent-ref %}

{% content-ref url="/pages/qMU4gxWjBoVtlplSiNpK" %}
[Connections](/build/connections)
{% endcontent-ref %}

{% content-ref url="/pages/wmavv2wpRINnHmfQxttZ" %}
[Workflows](/build/workflows)
{% endcontent-ref %}

{% content-ref url="/pages/udSXgbn9m4ToFHSWkJEr" %}
[Triggers](/build/triggers)
{% endcontent-ref %}

{% content-ref url="/pages/t1ZB16apkZ5UJn2FKF4J" %}
[Pending updates](/build/connector-updates)
{% endcontent-ref %}

{% content-ref url="/pages/XxuqmTRR0UCbPYxXrFQ5" %}
[MCP gateway](/build/mcp-gateway)
{% endcontent-ref %}

{% content-ref url="/pages/XN3OuSwdVOUG56y4CDuw" %}
[Common patterns](/build/patterns)
{% endcontent-ref %}


# Agent

The chat-driven builder that picks connectors, drafts workflows and shows you the diff.

**Integrations → Agent** (`/agent`), or the **What do you want to build?** prompt on Home

<figure><img src="/files/PGlM71d3tbrZAHBl0hqr" alt="The Agent screen at /agent: the Sessions rail on the left listing earlier sessions, and a Build an integration pane with four START FROM AN EXAMPLE cards above a Write a message composer carrying an Auto chip and a 0 / 1000k context meter"><figcaption><p><strong>Agent</strong> is the first item under Integrations in the sidebar. Home's prompt box opens the same place.</p></figcaption></figure>

The agent is the primary way integrations get built in fastn, and there are two ways in: the **Agent** item in the sidebar, directly under **Integrations**, or the **What do you want to build?** prompt on Home. Both land on the same screen, headed **Build an integration**, which states its own contract:

> Describe what you need in plain words. The agents pick the connectors, draft the workflow, and show you the diff before anything runs.

What it drafts is JavaScript — a `<slug>.js` module exporting `export default async function(ctx)`, opened in the [workflow editor](/build/workflows) for you to read, test and publish.

### Sessions

<figure><img src="/files/WpXbJ5jHkzXC3iFyhtWV" alt="An agent session: the Sessions rail on the left listing the current build and earlier sessions, the conversation on the right with a collapsed Worked · 3 steps summary, and the approval-mode chip and context meter under the composer"><figcaption><p>A session in progress. The rail on the left is your history; every session keeps its full conversation.</p></figcaption></figure>

The left rail, headed **Sessions**, holds your session history. Each session keeps its full conversation, so you can come back to an integration weeks later and continue where you left off rather than re-explaining it. Before you start anything it reads *No sessions yet. Click New session to start.*

* **New session** starts a fresh build.
* **Search sessions** (⌘K) filters the rail by name.
* **Collapse sessions sidebar** widens the workspace when you are reading a long diff.

### Starting from an example

Four cards sit under **START FROM AN EXAMPLE**, each pairing a goal with the concrete detail that makes it buildable:

| Card                             | What it asks for                                             |
| -------------------------------- | ------------------------------------------------------------ |
| **Sync deals into billing**      | Closed-won Salesforce opportunities, custom fields included  |
| **Alert before an SLA breaches** | Post to Slack when a Zendesk ticket is close to its deadline |
| **Keep a sheet current**         | Append new HubSpot contacts to a Google Sheet as they arrive |
| **Give an agent scoped access**  | Read-only Jira for one customer, nothing beyond that         |

They are written in the shape a good first message takes — a system, a trigger, and the specific thing that moves — and are worth reading before you write your own.

{% hint style="info" %}
Do not confuse these with the suggestion chips on **Home**, which are seeded per workspace and differ between them. The four cards above are the agent screen's own examples.
{% endhint %}

### Writing a good first message

The agent handles ambiguity by asking, but you get a better first draft by being specific about four things:

| Say                | Example                                                 |
| ------------------ | ------------------------------------------------------- |
| **The systems**    | "HubSpot", "Cin7 Core", not "our CRM"                   |
| **What starts it** | "when a deal is closed-won", "every night at 2am"       |
| **What moves**     | "the company name, the deal amount, and the line items" |
| **The rules**      | "skip anything under $500", "only the EU warehouse"     |

The composer carries **Attach a file**, the message box (*Write a message…*) and **Send**.

### What it does

The product's own summary is the reliable one: the agents pick the connectors, draft the workflow, and show you the diff before anything runs. In practice that means working out which systems are involved, reusing connectors that already exist and creating the ones that do not, handling authentication in the chat — inline API-key fields, or an OAuth form with client ID, secret and pre-filled scopes — and then writing the workflow code and opening it in the editor.

Generated test cases land on the editor's **Test cases** tab, grouped as `happy-path`, `pagination`, `fields`, `edge-cases` and `error-handling`, each row badged `LIVE` or `MOCK`.

### The integration plan

Before building, the agent writes an **Integration Plan** — an actual markdown document (named for the integration, such as `plan-hubspot-to-sheets.md`) that opens in a side panel. It sets out the trigger, the actions and connectors involved, a field-mapping table with a worked example per row, and a tenancy section. Its header has a **download** button, so the plan can be reviewed or circulated before anything is created.

Read it. It is the cheapest place to catch a wrong assumption — correcting a mapping here costs a sentence; correcting it after the workflow is generated costs a rebuild.

### Build progress

Alongside the plan, a **BUILD PROGRESS** panel tracks the work as a step count (`0/6`, `2/6`, …) across four phases:

| Phase             | What happens                                                                   |
| ----------------- | ------------------------------------------------------------------------------ |
| **1. Use cases**  | Plan and confirm the integration with you                                      |
| **2. Connectors** | Create and authorise the connectors it needs                                   |
| **3. Workflows**  | Approve mappings and test cases, build and test the workflow, bind the trigger |
| **4. Embed**      | Expose the finished integration as a widget                                    |

Each phase expands to its named substeps, a completed one collapses to a green check and **Complete**, and the one in flight shows a spinner against the current substep. Phase counts vary with the integration — a build with no customer-facing surface will not have an Embed phase.

### Clarifying questions

Where your brief is ambiguous, the agent asks rather than guesses, rendering the question as selectable answer cards — each with a title and a sentence explaining what choosing it means. Every question also offers **Other…** for a free-form answer, so you are never limited to the options it drafted. Typical questions cover sync scope (ongoing only, versus an initial backfill first) and tenancy (internal, versus per-customer multi-tenant).

### Reading what it did

The agent's tool calls collapse into a single summary line in the chat — **Worked · 3 steps**, with **Show details** to expand the individual calls. Documents it produces appear as artifact chips (for example *Field Mappings & Connectors*) that reopen the full panel.

Two counters are worth watching: the **context meter** beside the composer (`19k / 1000k`) shows how much of the session's context window the conversation has consumed, and **AI credits** in the top bar show what is left of your quota.

### Iterating

Follow-up messages refine what exists rather than starting over:

* "Add error handling for when the API is down"
* "Filter out records without an email address"
* "Change the schedule to hourly"
* "Notify Slack on failure"

The agent updates code, mappings and test cases together, so they do not drift apart.

### Attachments

**Attach a file** accepts an API spec, a sample payload, a field-mapping spreadsheet. Giving the agent a real payload is the single fastest way to get accurate mappings.

{% hint style="info" %}
Agent usage draws on the AI credits shown in the top bar — click it for the balance, an org total, and the reset date. Quota resets at the start of each calendar month, UTC. The popover also breaks usage down **By agent**, naming the ones doing this work: *Orchestrator V2 Orchestrator*, *Docs Agent*, *Error Diagnosis* and *Orchestrator V2 Title*. Plan and quota detail live under [Billing](/manage/billing), which is visible to Owners and Admins.
{% endhint %}

{% hint style="warning" %}
Using the AI assistant is gated by role rather than by an individual permission, so a role that cannot use it cannot be granted access to it one permission at a time.
{% endhint %}

### In this section

* [Approval mode](/build/agent/approval-mode)
* [Worked example](/build/agent/worked-example)


# Approval mode

How far the agent goes without checking in: Auto versus Manual.

The chip under the message box decides how far the agent goes without checking in.

| Mode     | Behaviour                                                              |
| -------- | ---------------------------------------------------------------------- |
| **Auto** | The default. Does not ask. Fastest for exploration.                    |
| Manual   | Asks before any create, update or delete. Use when touching live data. |

You can change it mid-session. Switching to **Auto** after a few approvals is a common pattern — watch what the agent reaches for while it is unfamiliar, then let it run.

### The approval gate

In **Manual** mode the agent stops and posts a card for each pending call. Nothing happens until you answer it.

<figure><img src="/files/bnJJWpDUUjyEiRztzzqv" alt="Two stacked approval cards, each headed Create connect link with the tool name create_connect_link, a JSON payload containing a connectorId, a VIEW RAW INPUT toggle, a note-to-agent textarea, and Accept, Always allow and Reject buttons"><figcaption><p>One card per pending call. Each shows the exact tool name and the exact arguments.</p></figcaption></figure>

Every card carries the same parts:

| Part                         | What it shows                                                                |
| ---------------------------- | ---------------------------------------------------------------------------- |
| Title                        | The action in plain language — e.g. **Create connect link**                  |
| Tool badge                   | The literal tool being invoked — e.g. `create_connect_link`                  |
| Payload                      | The exact JSON arguments, such as the `connectorId` it will act on           |
| **VIEW RAW INPUT**           | Expands the full, untruncated payload                                        |
| **NOTE TO AGENT (OPTIONAL)** | Free text — *"On reject, this note is sent to the agent so it can re-plan…"* |

### The three responses

* **Accept** — runs this one call, and only this one. The next call gates again.
* **Always allow** — stops asking for *that tool* for the remainder of the session. Useful once you have seen a read-only call a few times; think harder before using it on a tool that writes.
* **Reject** — refuses the call. Whatever you typed in the note goes back to the agent, which re-plans around it.

{% hint style="info" %}
The note is what makes rejection useful. *"Use the sandbox sheet, not the production one"* produces a corrected plan. A bare **Reject** with no explanation usually produces the same call again.
{% endhint %}

{% hint style="warning" %}
**Always allow** is scoped to the session, not to the tool forever — but within a long session it can cover a lot of ground. If the agent is about to touch production data, leave the gate in place.
{% endhint %}

See [the worked example](/build/agent/worked-example) for a gate in the middle of a real build.


# Worked example

A real HubSpot-to-Google-Sheets build, from plain-words prompt to bound trigger.

This is an actual run, screen by screen. The request was one sentence:

> When a new contact is created in HubSpot, add a row to a Google Sheet with their name, email, and company.

Everything below is what the agent did with it.

{% stepper %}
{% step %}

#### Describe it in plain words

Type the whole thing into **What do you want to build?** on Home and send it. There is no need to name connectors, pick a trigger, or know what a workflow is yet.

<figure><img src="/files/WpXbJ5jHkzXC3iFyhtWV" alt="An agent session: the sent prompt at top right, a collapsed Worked · 3 steps summary beneath it, the Sessions rail on the left, and the Manual approval-mode chip under the composer"><figcaption><p>The session opens with your message at the top. The agent's tool runs collapse into <strong>Worked · 3 steps</strong> — <strong>Show details</strong> expands them.</p></figcaption></figure>

The session is added to the **Sessions** rail on the left, named after your prompt, so you can leave and come back to it.

Three things on this screen are worth knowing before you go further:

* **The approval-mode chip** sits under the composer — here set to **Manual**. It decides whether the agent pauses before acting. See [Approval mode](/build/agent/approval-mode).
* **The context meter** on the right (`19k / 1000k`) shows how much of the session's context window the conversation has used.
* **AI credits** in the top bar (`50 of 50 credits left`) are what agent runs draw down.
  {% endstep %}

{% step %}

#### It asks before acting — because Manual mode was on

The agent needed to create connect links for the two systems. In **Manual** mode it does not just do that; it stops and shows you each call.

<figure><img src="/files/bnJJWpDUUjyEiRztzzqv" alt="Two stacked approval cards, each headed Create connect link with the tool name create_connect_link, a JSON payload containing a connectorId, a VIEW RAW INPUT toggle, a note-to-agent textarea, and Accept, Always allow and Reject buttons"><figcaption><p>A Manual-mode approval gate. One card per pending call, each showing the exact tool and payload.</p></figcaption></figure>

Every gate shows you the same five things:

| On the card                  | What it is                                                                   |
| ---------------------------- | ---------------------------------------------------------------------------- |
| **Create connect link**      | The action in plain language                                                 |
| `create_connect_link`        | The literal tool being called                                                |
| The JSON body                | The exact arguments — here the `connectorId` it is acting on                 |
| **VIEW RAW INPUT**           | Expands the untruncated payload                                              |
| **NOTE TO AGENT (OPTIONAL)** | Free text — *"On reject, this note is sent to the agent so it can re-plan…"* |

And three ways out: **Accept** runs this one call, **Always allow** stops asking for that tool for the rest of the session, and **Reject** refuses it — with your note attached, so the agent re-plans rather than simply retrying.

{% hint style="info" %}
The note is the useful part of rejecting. *"Use the sandbox sheet, not the production one"* gets you a corrected plan; a bare reject just gets you a stuck agent.
{% endhint %}
{% endstep %}

{% step %}

#### It asks what you actually meant

Rather than guessing at the ambiguous parts, the agent puts the question back to you as selectable cards. This run got two.

**How much should it sync?**

* **Ongoing sync only** — *Sync new contacts as they are created in HubSpot from now on.*
* **Initial import + ongoing sync** — *Import all existing HubSpot contacts into the spreadsheet first, then continue syncing new ones in real time.*
* **Other…** — *Type a free-form answer.*

**Who is it for?** — the tenancy question, answered here with **For customers (Multi-tenant)**.

Those two answers change the generated code materially, which is exactly why it asks instead of assuming. Every question also offers **Other…**, so you are never boxed into the options it drafted.
{% endstep %}

{% step %}

#### It writes a plan you can read before anything is built

The agent produces an **Integration Plan** — a real markdown document, here `/plan-hubspot-to-sheets.md` — in a side panel, and a **BUILD PROGRESS** tracker beside it.

<figure><img src="/files/S3MGugVkpoS2BGheamGX" alt="Three panels: the clarifying-question answer cards on the left, a BUILD PROGRESS tracker reading 0/6 with phases Use cases, Connectors, Workflows and Embed in the middle, and the Integration Plan document on the right showing Overview, Flow Details and a Field Mapping table"><figcaption><p>The plan (right) and the build tracker (middle). Nothing has been built yet — the counter reads <code>0/6</code>.</p></figcaption></figure>

The plan spells out the mechanism, not just the intent:

1. **Trigger** — the real-time `contact.creation` event from HubSpot.
2. **Fetch details** — retrieve the contact's full profile (`firstname`, `lastname`, `email`, `company`) using HubSpot's `getContact` action.
3. **Target action** — Google Sheets `appendValues`, appending `[First Name + Last Name, Email, Company]` to the designated tab.
4. **Delivery** — ongoing real-time sync, with an optional initial backfill of existing contacts.

Below that sits a **Field Mapping** table with a worked example per row, and a **Tenancy** section reflecting the multi-tenant answer:

| HubSpot contact field              | Google Sheet column | Example                |
| ---------------------------------- | ------------------- | ---------------------- |
| First Name + Last Name / Full Name | Name                | Jane Doe               |
| Email (`email`)                    | Email               | <jane.doe@example.com> |
| Company (`company`)                | Company             | Acme Corp              |

The panel header carries a **download** button — the plan is a portable artifact, useful for review before you let it build.
{% endstep %}

{% step %}

#### It builds in tracked phases

**BUILD PROGRESS** is the honest view of what is happening. It counts completed steps against the total (`0/6` → `2/6` → …) across four phases, each expandable into named substeps.

<figure><img src="/files/MMmzBUYKw3VUbmSmgKjx" alt="The BUILD PROGRESS tracker at 2 of 6: Use cases and Connectors marked Complete with green checks, Workflows in progress showing three substeps, and Embed pending; the chat on the left shows a Proposed Field Mappings message"><figcaption><p>Two phases complete. <strong>Workflows</strong> is mid-flight, its three substeps named. Note the chip now reads <strong>Auto</strong>.</p></figcaption></figure>

| Phase             | Substeps in this run                                                                                                                      |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **1. Use cases**  | Plan & confirm HubSpot to Google Sheets contact sync                                                                                      |
| **2. Connectors** | Create and authorise the two connectors                                                                                                   |
| **3. Workflows**  | Review & approve field mappings and test cases · Build and test contact sync workflow · Bind HubSpot `contact.creation` app event trigger |
| **4. Embed**      | Expose the integration as a widget                                                                                                        |

A completed phase collapses to a green check and **Complete**. The one in flight shows a spinner against the current substep, so you always know which of the six things it is doing.

Notice that the last substep of **Workflows** is binding the trigger. The agent does not stop at generated code — it wires the thing that makes it run.
{% endstep %}

{% step %}

#### It brings the mappings back for approval

Before writing the workflow, the agent posted its **Proposed Field Mappings** into the chat, with a **Field Mappings & Connectors** artifact chip that reopens the full document:

* **Name** — combines the HubSpot contact's `firstname` and `lastname` into the Name column.
* **Email** — maps the contact's `email` to the Email column.
* **Company** — maps the contact's `company` to the Company column.

This is the moment to correct it. Replying *"put first and last name in separate columns"* here is far cheaper than editing generated code afterwards — and the agent updates the code, the mappings and the test cases together, so they cannot drift apart.
{% endstep %}

{% step %}

#### Then test and publish as usual

What the agent hands over is an ordinary workflow. It opens in the [workflow editor](/build/workflows) as a `<slug>.js` module, its generated scenarios sit on the **Test cases** tab, and you run it from the **Test** tab. Publishing a snapshot and deploying it are the same steps as for anything you wrote by hand — see [Lifecycle](/build/workflows/lifecycle).
{% endstep %}
{% endstepper %}

### What this run tells you about the agent

* **It asks rather than assumes.** Two ambiguities in a one-sentence brief became two explicit questions, each with an **Other…** escape hatch.
* **It plans in writing first.** The Integration Plan is readable, downloadable, and exists before any building starts.
* **It shows its work.** Six tracked steps across four named phases — not a spinner and a promise.
* **It finishes the job.** Binding the app-event trigger is part of the build, not homework left for you.
* **Manual mode is a real gate.** Every call surfaced its exact tool name and payload, with a note field that turns a rejection into a re-plan.

{% hint style="info" %}
Manual mode is worth the extra clicks the first few times, purely because the approval cards show you which tools the agent reaches for. Once the shape is familiar, switch to **Auto** — as this run did partway through — and let it work.
{% endhint %}


# Connectors

Every system your customers can authorise — managed, imported or built by you.

**Integrations → Connectors** · `/integrations?tab=connectors`

<figure><img src="/files/L7m71FNQxZhSfr9lip3F" alt="The Connectors catalogue showing 24 of 354 cards — AbstractAPI Email Reputation, AbstractAPI Holidays, Adyen, Agile CRM, Aha!, Airship — each badged managed and Managed by Fastn, with its own Connect button"><figcaption><p>Import and Create connector sit top-right; the chips filter by All, Connected, OAuth and visibility.</p></figcaption></figure>

A connector is the definition of one external system: its actions, its authentication methods, its webhook configuration and its versions. It is not a credential — that is a [connection](/build/connections).

### In this section

* [The catalogue](/build/connectors/the-catalogue)
* [Inside a connector](/build/connectors/inside-a-connector)
* [Connect a system](/build/connectors/connect-a-system)
* [Action detail](/build/connectors/action-detail)
* [Creating a connector](/build/connectors/creating-a-connector)
* [Importing and exporting](/build/connectors/importing-and-exporting)


# The catalogue

How the connector catalogue reads: badges, filters, card anatomy, and the counts that mislead.

The page header states the intent plainly:

> Every system your customers can authorise. Depth on the ones that block deals, not a catalogue count.

Connectors marked **managed** are maintained by fastn — when the vendor ships a breaking change, you get a proposal under [Pending updates](/build/connector-updates) rather than a broken sync. Ones you build yourself are badged **Custom**. Either badge is replaced by **Connected** once at least one connection exists.

| Control                     | What it filters                                              | URL            |
| --------------------------- | ------------------------------------------------------------ | -------------- |
| **Search connectors**       | Name and description                                         | `?q=`          |
| **All / Connected / OAuth** | Everything · at least one live connection · offers OAuth 2.0 | `?category=`   |
| **All Visibility**          | `All Visibility`, `Private`, `Public`                        | `?visibility=` |

There is no sort control. The list pages at 24 per page with a footer reading `1–24 of 354`; the page number is not kept in the URL, so a deep link always lands on page one.

A search that matches nothing shows `No connectors match "x"`, *Try another search or category.* and a **Clear search** button.

**Card anatomy.** Favicon, name, badge, description, an `OAuth 2.0` chip where it applies, and a provenance string. The footer button reads **Connect**, or **Add another connection** with a chevron offering **Reconnect** and **Disconnect**. The `⋯` menu holds **Select**, **Edit**, **Export** and **Delete**.

**Header controls.** **Create connector** opens the create dialog. **Import** is a bare file input — it takes a JSON connector definition with no intermediate dialog. Selecting cards (via `⋯ → Select`) reveals **Export Selected (n)**.

{% hint style="warning" %}
Three things about this list are known to mislead, and are worth knowing before you count anything:

* The catalogue contains duplicates — Asana, HubSpot, Salesforce, Slack, Notion and Cin7 Core each appear twice, once `managed` and once `Custom` — so the total is not a count of distinct systems.
* A connector badged `Connected` in the list can still report `0 connections` on its own detail page.
* Provenance is written three different ways for the same thing: *Managed by Fastn*, *Managed by fastn.ai* and *Managed by fastn*.
  {% endhint %}


# Inside a connector

The three-pane connector detail view and its five tabs.

`/integrations/connectors/<slug>`. Three panes: the connector list on the left, that connector's actions in the middle, and the detail tabs on the right.

* **Left** — **Back to all connectors**, **Create a connector**, a search box, and the connector list with `<Name> operations` expanders. Connectors your org owns also get **Add action**.
* **Middle** — the action list, each with a method chip (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`), plus **Select all**. Selecting actions is how you scope what a workflow or an agent may use — the point of *read-only Jira for one customer, nothing beyond that*.
* **Right** — the connector name, a `<owner> · <auth type>` line, a version chip such as `v1.0 · Test`, a **Connect** button, and a `⋯` menu with **Edit** and **Delete**. Below that, five tabs.

<figure><img src="/files/wdK1HneWPkiBaOpWiUV7" alt="The HubSpot connector&#x27;s Overview tab: tiles reading 0 Connections, 1 Auth method and v1.0 Current version, over a Details table giving slug hubspot, Visibility Public and Created Aug 20, 2026"><figcaption><p>The version tile reads <em>In test, not published</em> — the connector works, but no customer can reach it yet.</p></figcaption></figure>

#### Overview

Three tiles — **Connections**, **Auth method(s)**, **Current version** — and a details table.

| Field            | Meaning                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| **Slug**         | The identifier used in API paths and in workflow code.                                           |
| **Visibility**   | Private or Public. New connectors start Private; publishing publicly is a platform-admin action. |
| **Auth methods** | What your customers authorise with.                                                              |
| **Created**      | When it entered the catalogue.                                                                   |

A **Versions** section underneath carries a `Test` / `Live` toggle and a **Publish to live** control.

#### Auth

<figure><img src="/files/IV0gNuD6IaYyIivlwVxs" alt="The HubSpot connector&#x27;s Auth tab, headed Authentication methods, with a single row: OAuth 2.0 badged Default, 1 provider, and a View providers button top-right"><figcaption><p>The providers link opens the OAuth apps sitting behind that method.</p></figcaption></figure>

One row per authentication method, each showing its provider count, plus a **View providers** button that opens the OAuth apps behind them. The tab describes each shape in your customer's terms:

> Your customers sign in with the provider and approve access.

> Your customers paste a key they generate themselves.

#### Connections

Every customer who has authorised this connector. Before anyone has, it reads `Nobody has connected yet`. It is the same data as the workspace-wide [Connections](/build/connections) page, filtered to this one system.

#### Version pins

> Hold one customer on one version while everyone else moves on. A version set in code still wins over a pin.

That second sentence is the rule that matters: a pin is a fallback, not an override. Pin a customer when they cannot absorb a change yet — a field they depend on moved, or their own integration needs a release first — then unpin them when they are ready.

With nothing to pin, the tab reads `No versioned actions yet` / *Add actions with an externalVersion to enable per-tenant routing.*

The tab also carries a **Compare two versions** tool: `Action slug`, `From`, `To`, `From major`, `To major`, and **Compare**.

#### Webhook config

**New config** creates one. Until then:

> No webhook config yet

> Until one exists, customers of this connector can be polled but cannot be notified.

That is the plumbing behind an [app event trigger](/build/triggers) — configure it once here and app event triggers on this connector work for every customer, rather than each of them registering a webhook themselves.


# Connect a system

Making a connection, from picking the connector to confirming it landed.

Making a connection is how a connector goes from a definition to something that can actually call an API. There are two ways in — the **Connect** button on the connector itself, or **New connection** on the [Connections](/build/connections) page — and both lead to the same authorisation flow.

{% stepper %}
{% step %}

#### Pick the connector

Press **Connect** (or **Add another connection**) on the connector's card or detail header, or open **New connection** and find the system in the **Connect a system** picker — a searchable, A–Z list of every connector in the catalogue.
{% endstep %}

{% step %}

#### Choose the authentication method

If the connector offers more than one method, choose one. The **Auth** tab (above) describes the two shapes in the customer's own terms — *Your customers sign in with the provider and approve access* for OAuth, and *Your customers paste a key they generate themselves* for an API key.
{% endstep %}

{% step %}

#### Provide the credential

**OAuth** sends you to the provider to sign in and approve the requested access; fastn receives the tokens. An **API key** (and the other keyed methods) is entered in the connect form itself. Either way, fastn stores the credential encrypted and never displays it again.
{% endstep %}

{% step %}

#### Confirm it landed

A completed connection appears on the connector's **Connections** tab — which reads `Nobody has connected yet` until the first one — and on the workspace-wide [Connections](/build/connections) page, where you can check its status and **Reconnect** or **Disconnect** it.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
Customer connections are meant to be made by the customer, through your embedded widget — that is what keeps their credentials theirs. Use **New connection** for links your own organisation owns, such as your own Slack workspace.
{% endhint %}


# Action detail

The seven tabs behind a single action, and how platform-owned actions differ.

Opening an action from the middle pane gives seven tabs: **Params**, **Headers**, **Auth**, **Body**, **Input schema**, **Output schema** and **Mocks**. The footer shows the action's version and a **New version** button.

Actions on a platform-owned connector are marked **Read-only — owned by platform** and offer **Propose an update** instead of an edit — that proposal is what surfaces under [Pending updates](/build/connector-updates).


# Creating a connector

The Create connector dialog: identity, connection and authentication in one place.

**Create connector** opens a dialog with three sections: **Identity**, **Connection** and **Authentication**.

<figure><img src="/files/Sg5Lh3VKyb1u1m0r6OcQ" alt="The Create a connector dialog: Identity fields Name, Slug and Description, then Connection fields Protocol set to REST, Visibility set to Private, Domain and Icon URL"><figcaption><p>Create connector stays disabled until the name is filled — the footer says so.</p></figcaption></figure>

| Field           | Type   | Notes                                                                               |
| --------------- | ------ | ----------------------------------------------------------------------------------- |
| **Name**        | text   | Required. What people see. Placeholder `Salesforce`.                                |
| **Slug**        | text   | Required. *Derived from the name. Edit it to override.* Used in API paths and code. |
| **Description** | text   | Optional, but the agent reads it when deciding what a connector is for.             |
| **Protocol**    | select | Required. `REST` (default), `MCP`, `FTP`, `Database`, `REDIS`.                      |
| **Visibility**  | select | Offers only `Private` — publishing a connector publicly is a platform-admin action. |
| **Domain**      | text   | Optional. The vendor's domain, e.g. `salesforce.com`.                               |
| **Icon URL**    | text   | Optional. Shown on the card and in the widget.                                      |

Authentication is set up **in the same dialog**, not afterwards. **Add method** adds one; each method has a type:

| Type                | Internal value |
| ------------------- | -------------- |
| No Auth             | `NO_AUTH`      |
| Basic Auth          | `BASIC`        |
| Digest Auth         | `DIGEST`       |
| Bearer Token        | `BEARER`       |
| API Key             | `API_KEY`      |
| OAuth 2.0 (default) | `OAUTH_2`      |
| Custom              | `INPUT`        |

Those internal values are worth knowing because some of them surface raw in the `Auth` column on [Connections](/build/connections).

Each method also carries a **Set as default** radio, an **Authentication docs URL(optional)** field, a **Use Dynamic Client Registration (DCR)** checkbox (RFC 7591), and an **Additional OAuth Config(optional)** key/value repeater. Choosing Basic Auth or API Key swaps in a **Configuration** section with a `Form` / `JSON` toggle and a key/value repeater.

The dialog footer will not let you save until the connector is named — the hint reads *Give it a name to continue.*

{% hint style="info" %}
You rarely need to do this by hand. Describe the system to the [Agent](/build/agent) and give it a spec URL or an OpenAPI file — it will discover the actions, build the connector, and test it.
{% endhint %}


# Importing and exporting

Moving connector definitions in and out as JSON.

**Import** takes a JSON connector definition straight from a file picker — exported from another workspace, or generated from a spec. In the other direction, `⋯ → Export` exports one card, and **Export Selected (n)** exports a batch.


# Unified APIs

One canonical endpoint per business entity, served by whichever providers your customers connect.

**Integrations → Unified APIs** · `/integrations?tab=unified`

<figure><img src="/files/WmhVDGtf9tZrWkhJNU0j" alt="The Unified APIs page showing five category cards — CRM with 10 entities, Documents with 2, Knowledge Base with 1, Messaging with 2 and Project Management with 1 — each stacking its provider icons"><figcaption><p>The icons on each card are the providers currently serving that category.</p></figcaption></figure>

Three of your customers use three different CRMs. Without a unified API, your code branches three ways for what is conceptually one operation: create a contact. With one, you call a single endpoint and fastn routes to whichever provider that customer authorised.

> One canonical endpoint per business entity, served by whichever providers your org connects.

### Categories and entities

A **category** is a domain. An **entity** is a business object inside it. Each entity is backed by one or more **providers**. There are five categories, shown with a `5 categories` chip and a **Search categories** box: **CRM**, **Documents**, **Knowledge Base**, **Messaging** and **Project Management**. A search that matches nothing reads `No categories match` / *Try another search.*

| Category      | Entities                                                                                                      | Providers (slugs)                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| **CRM**       | `Account`, `Contact`, `Engagement`, `Engagement Type`, `Lead`, `Note`, `Opportunity`, `Stage`, `Task`, `User` | 12 providers, including `hubspot`, `salesforce` and `zohoCrm` |
| **Documents** | `Document`, `Document Content`                                                                                | `googleDocs`, `notion`                                        |
| **Messaging** | `Channel Message`, `Direct Message`                                                                           | `microsoftTeams`, `slack`                                     |

The provider slugs are what you use in the API; the cards show display names.

Knowledge Base and Project Management are the two newest categories; their entities and providers are shown when you open them in the product.

### In this section

* [Inside a category](/build/unified-apis/inside-a-category)
* [Unified vs direct](/build/unified-apis/unified-vs-direct)


# Inside a category

Opening a category: entities, connection state, providers and endpoints.

<figure><img src="/files/xN0fuXOSb1ChDkylQzFE" alt="The CRM category, chips reading 10 entities and 12 providers: the Account entity at 0/12 connected, twelve provider rows from Capsule CRM to Zoho CRM each offering Connect, then the endpoint list"><figcaption><p>Nothing is connected here yet, and the endpoints are listed anyway — they exist per entity, not per provider.</p></figcaption></figure>

Opening a category swaps the pane for a detail view with the breadcrumb **Unified APIs / \<Category>** and two chips, *N entities* and *N providers*. **The URL does not change when you do this**, so there is no link you can send someone that opens a category directly.

Each entity gets its own block showing:

* **Connection state** — `0/12 connected` on CRM's `Account`, `0/1 connected` elsewhere. How many of the backing providers this customer has authorised.
* **PROVIDERS** — one row per provider, with a **Connect** button, or **Connected** and a `⋯` menu offering **Add another connection** and **Disconnect default**.
* **API** — the endpoints, filtered by an **All providers** selector, each row with **Copy curl**.

Endpoints follow a consistent shape:

```http
GET  /api/v1/unified/crm/account?page_size=50
GET  /api/v1/unified/crm/account/RECORD_ID
POST /api/v1/unified/crm/account
```

```http
POST /api/v1/unified/crm/note
```

Not every entity supports every verb. `Note` and both Messaging entities — `Channel Message` and `Direct Message` — are create-only.


# Unified vs direct

When to reach for a unified API and when to call the connector directly.

| Use a unified API when                                    | Use the connector directly when                      |
| --------------------------------------------------------- | ---------------------------------------------------- |
| Several providers do the same job for different customers | You need a field or action only one provider has     |
| You want one code path regardless of the customer's stack | The operation has no meaningful equivalent elsewhere |
| The fields you need are common across providers           | You are already deep in one vendor's model           |

The two are not exclusive — a workflow can use a unified endpoint for the common path and a direct connector action for the vendor-specific part.

### The All providers filter

Above the endpoint list, **All providers** shows the canonical surface; picking a single provider shows how that one behaves. Useful when a provider has a quirk you need to design around.


# Connections

Authenticated links between your customers and their systems.

**Integrations → Connections** · `/integrations?tab=connections`

<figure><img src="/files/fjpiiF1xaPxfVaOScHW3" alt="The Connections table, 5 connections, under Connector, Customer, Auth, Status and Created: TikTok Shop on OAUTH, Cin7 Core and three unnamed connector IDs on INPUT, every row Active with an em dash for Customer"><figcaption><p>The Auth column shows the raw internal values — <code>OAUTH</code> and <code>INPUT</code> here. A connector with no catalogue entry shows its raw ID instead of a name.</p></figcaption></figure>

A connection is one customer's authorised link to one connector. It holds the credential — encrypted, never displayed — and records how it was obtained.

### How connections work

A connection is an authenticated link between one of your customers and one connector — the stored, encrypted result of that customer authorising access once, which every later API call reuses so nobody signs in again. A few properties are worth holding in mind before the detail below:

* **Who it belongs to — Scope.** Most connections belong to a single customer (one tenant). Some belong to your organisation instead: those read `Account level` on the detail page, meaning the link is shared across the workspace rather than tied to one customer.
* **How you address it — the connection ID.** Every connection has an id of the form `ucl:org_<org>:<env>:<connectorId>:<authId>:<tenant>`. You pass it to the API to act as that customer, and it is what routes a call to the right credential.
* **Whether it still works — Status.** A connection is `Active`, `Inactive`, `Expired` or `Failed`. Active needs nothing; the other three need attention.
* **Fixing or ending one — Reconnect / Disconnect.** Every row's `⋯` menu offers **Reconnect** (re-run authorisation to restore a broken link) and **Disconnect** (syncing stops and the credential is deleted).
* **Making one yourself — the picker.** **New connection** opens the full-screen **Connect a system** picker; customer connections should instead come through your embedded widget.

The rest of this page is the detail behind each of those.

### The table

| Column        | What it tells you                                   |
| ------------- | --------------------------------------------------- |
| **Connector** | The system, with the tenant key on the second line. |
| **Customer**  | Which customer owns it.                             |
| **Auth**      | How it was authorised.                              |
| **Status**    | Active, Inactive, Expired or Failed.                |
| **Created**   | When the customer authorised it.                    |
| **⋯**         | **Reconnect** and **Disconnect**.                   |

The table pages at 10 rows. Filter chips above it — **All**, **Active**, **Inactive**, **Expired**, **Failed** — are not kept in the URL, so a filtered view cannot be linked.

{% hint style="warning" %}
The `Active`, `Inactive`, `Expired` and `Failed` chips currently return nothing, even when every row in the unfiltered table shows `Active`. Until that is fixed, triage from the full list rather than the chips.
{% endhint %}

### In this section

* [Statuses](/build/connections/statuses)
* [Auth types](/build/connections/auth-types)
* [Inside a connection](/build/connections/inside-a-connection)
* [Creating a connection](/build/connections/creating-a-connection)


# Statuses

What Active, Inactive, Expired and Failed mean, and what to do about each.

| Status       | Meaning                                                                                  | Action                                                                          |
| ------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| **Active**   | Working.                                                                                 | Nothing.                                                                        |
| **Inactive** | Exists but disabled.                                                                     | **Reconnect** from the row menu, or **Disconnect** if it is genuinely finished. |
| **Expired**  | The credential ran out and could not be refreshed.                                       | The customer re-authorises through your widget.                                 |
| **Failed**   | The last verification call was rejected — revoked access, changed password, rotated key. | Same: the customer reconnects.                                                  |

{% hint style="info" %}
Expired and Failed connections are the most common cause of "the sync stopped working". Watch them, or set an [alert](/operate/alerts) on broken connectors so you hear about it before your customer does.
{% endhint %}


# Auth types

The two shapes a connection's credential takes: OAuth and Custom/INPUT.

The `Auth` column mixes display names and raw enum values for the same concepts — you will see `OAuth 2.0` and `OAUTH` for one, `Custom` and `INPUT` for the other. They are the same two shapes:

**OAuth** — the customer signed in with the provider. fastn holds the refresh token and renews access tokens on its own. Whether that is still working is visible on the connection's detail page, under **Token and activity**.

**Custom / INPUT** — the customer pasted a key, token or connection string. These do not expire on their own but do break when rotated upstream.


# Inside a connection

The four sections of a connection's detail view.

Opening a row gives four sections.

**Connection** — `Customer`, `Connector`, `Auth method`, `Scope` and `Connection ID`. Scope reads `Account level` when the connection is shared across the workspace rather than belonging to one customer. The connection id has the form:

```
ucl:org_<org>:<env>:<connectorId>:<authId>:<tenant>
```

The page's own note on it is the operative one:

> Pass this to the API to act as this customer.

**Token and activity** — `Expires`, `Last refreshed`, `Last used`, `Created`, `Updated`. This is where you check whether a refresh is still succeeding.

**Recent activity** — the last calls made on this connection, with **View all** into [Activity](/operate/operate).

**Danger zone** — **Disconnect this customer**:

> Syncing stops immediately and the credential is deleted.


# Creating a connection

Making a connection yourself with New connection, and when to.

**New connection** opens a full-screen **Connect a system** picker: a **Search systems** box, an A–Z index, a counter reading `354 of 354 systems`, and one row per connector showing its auth-method label.

Use it for connections your organisation owns — your own Slack workspace, your own data warehouse — which a workflow then uses at account level rather than per customer.

Customer connections should be created by the customer, through your embedded widget. That is what keeps their credentials theirs.


# Workflows

The code that runs on a trigger, a schedule, or an agent call.

**Integrations → Workflows** · `/integrations?tab=workflows`

<figure><img src="/files/txSRheJZvREF6aobLFKq" alt="5 workflows under Workflow, Status, Latest, live and Updated: three Load Test rows Active at v1, one badged 2 triggers off, and sync_tiktok_settlements_cin7 Not published and Unpublished"><figcaption><p>Connect GitHub and Create workflow sit top-right; the pills filter by execution tier. Every row's <strong>live</strong> cell is an em dash — nothing is deployed.</p></figcaption></figure>

A workflow is JavaScript. One file, `<slug>.js`, exporting one function:

```javascript
export default async function (ctx) {
  // ctx.input, ctx.headers, ctx.connectors
}
```

There is no node palette and no drag-and-drop step builder. Everything else on this screen configures, describes, tests or deploys that function.

### The list

| Column          | Notes                                                               |
| --------------- | ------------------------------------------------------------------- |
| checkbox        | Selects rows.                                                       |
| **Workflow**    | Name, slug and description.                                         |
| **Status**      | `Active` or `Not published`.                                        |
| **Latest**      | `v2`, or `Unpublished` when no snapshot exists yet.                 |
| **live**        | Which version is deployed to the live environment, if any.          |
| **Updated**     | Last change.                                                        |
| **Run** + **⋯** | Run executes on demand; the `⋯` menu holds **Edit** and **Delete**. |

Three status tooltips explain the unpublished state, and the middle one is the string you will search for when a call fails:

> Never published — this workflow cannot run yet.

> Every call returns WORKFLOW\_NOT\_PUBLISHED until a snapshot is published

> Nothing deployed to Live.

**Search workflows** filters by name; pills split the list by execution tier — **All**, **Instant**, **Standard**, **Long**.

Two buttons sit top-right: **Connect GitHub**, which connects the workspace to a GitHub repository, and **Create workflow**, which gives you an empty one.

### In this section

* [The editor](/build/workflows/the-editor)
* [The tabs](/build/workflows/the-tabs)
* [Lifecycle](/build/workflows/lifecycle)


# The editor

The workflow editor walked through tab by tab, plus the configuration panel reference.

<figure><img src="/files/UbzXv9FlzrDebk2xOhId" alt="The workflow editor with code editing enabled: a Configuration panel on the left with Name and Slug fields, workflow.js in the middle labelled JavaScript · export default async function(ctx), and the Test panel on the right showing ctx.input, ctx.headers, Use contract, a Live selector and Run Live"><figcaption><p>The editor with code editing enabled — Configuration left, <code>workflow.js</code> centre, the tool tabs right.</p></figcaption></figure>

Opening a workflow — **Create workflow**, or **Edit** from a row's `⋯` menu — drops you into a drawer with **Configuration** on the left and the tool tabs on the right. Whether there is a code column between them depends on your workspace.

{% hint style="warning" %}
**Code editing is switched off in almost every workspace.** It is enabled only for the parent organisation, so unless you are working there, you will not see the middle column at all — the editor shows Configuration and the tool tabs, and the banner reads:

> Code editing is switched off for this workspace — workflows here are generated and updated by the AI builder. It can be switched on if you want to write workflow code yourself; ask fastn to enable it.

Everything else on this page still applies. You change a workflow's behaviour by talking to the [agent](/build/agent) instead of typing into the file, and you still configure, test, wire connectors, edit the contract, publish and deploy exactly as described below. **If you want to write workflow code yourself, ask fastn to switch it on.**
{% endhint %}

The walkthrough below assumes the code column is present. This is roughly the order you move through it.

{% stepper %}
{% step %}

#### Set up Configuration

Under **Identity**, give it a **Name** (required) and, on a new workflow, a **Slug** — the slug is fixed once the workflow exists, so choose it deliberately. **Description** is free text. Under **Execution**, pick an **Execution tier**: `Instant` runs synchronously and caps at 30s, `Standard` returns `202` and runs up to 15 min, `Long` returns `202` and runs up to 36 h. The **Execution timeout** slider is then constrained to that tier.

Full ranges and defaults are in the Configuration panel reference below.
{% endstep %}

{% step %}

#### Decide what happens when a run fails

**Retry policy** is off by default; turning it on exposes **Max attempts**, **Initial interval (ms)**, **Backoff coefficient** and **Max interval (ms)**. It only retries transient failures — code errors, data errors and out-of-memory never retry. **Escalate on timeout** is a separate toggle that retries one tier up on a timeout, and is hidden on the `Long` tier.
{% endstep %}

{% step %}

#### Write the function

The middle column is one `<slug>.js` file exporting `export default async function (ctx)`. You reach the request through `ctx.input` and `ctx.headers`, and connected systems through `ctx.connectors` and `fastn.*`. A new workflow is scaffolded for you:

```javascript
export default async function(ctx) {
  const { input, headers } = ctx;
  // Your workflow logic here
  return { result: "Hello from workflow!", input };
}
```

**This step is the one that does not apply in most workspaces.** With code editing off — the default everywhere but the parent organisation — the file is generated and updated by the AI builder, and you describe the change you want to the [agent](/build/agent) rather than editing it here. Skip to the next step; everything from there on is identical.
{% endstep %}

{% step %}

#### Test it — `Test` tab

Put a body in the `ctx.input` editor and headers in `ctx.headers`, or press **Use contract** to fill the input from the declared contract. Pick what the run actually touches — `Live`, `Partial Mock` or `Fully Mock` — then **Run Live** (or **Run**).
{% endstep %}

{% step %}

#### Check the wiring — `Connectors` tab

Every `fastn.connectors.X.Y(…)` call in your code is extracted here each time you save, so this tab is a readout of what the workflow actually talks to. **Add** covers anything you need to wire by hand.
{% endstep %}

{% step %}

#### Declare the shape — `Contract` tab

**Input contract** and **Output contract**, each viewable as **JSON** or **Schema**. Paste an example payload in the JSON view and the schema is generated live; the schema is the contract, and is hand-editable.
{% endstep %}

{% step %}

#### Read the logic back — `Diagram` tab

**Flow**, **Sequence** and **Docs** are all generated from the code and read-only — you edit the code and they follow. **Docs** carries an **End user** / **Technical** toggle and consumes AI credits when you **Regenerate**.
{% endstep %}

{% step %}

#### Confirm the scenarios — `Test cases` tab

The cases drafted during the build, grouped `happy-path`, `pagination`, `fields`, `edge-cases` and `error-handling`, each row an id such as `TC-01` with a `LIVE` or `MOCK` badge.
{% endstep %}

{% step %}

#### Watch it run — `Executions` tab

This workflow's own run history, filtered by the HTTP-status pills `All`, `200`, `201`, `400`, `404`, `422`, `500`. The neighbouring **Sync reports** tab fills in the first time the workflow runs `fastn.diff.compare`.
{% endstep %}

{% step %}

#### Hand it to callers — `API` and `Docs` tabs

**API** gives the `POST …/api/v1/workflows/<wfId>/execute` endpoint with a copyable curl and the two headers that steer it, `X-fastn-Test-Mode` and `x-fastn-env`. **Docs** is the runtime reference for what `ctx` and `fastn.*` expose inside the function.
{% endstep %}
{% endstepper %}

### The editor

Opening a workflow opens a drawer laid out as **configuration on the left, the tool tabs on the right, and — where code editing is enabled — the code editor between them**. That middle column is labelled `<slug>.js` and `JavaScript · export default async function(ctx)`. A workspace without code editing gets the same drawer minus that column.

Along the top: **Discard**, the workflow name, **Close tab**, **Save workflow** and **Publish**. Closing with unsaved edits asks first — *Discard unsaved changes?* / "This workflow has edits that have not been saved. Closing the tab throws them away." / **Keep editing** / **Discard changes**.

<figure><img src="/files/eh3hpjrGXEkDz3iSOv09" alt="The workflow Configuration panel scrolled to a Status toggle reading Enabled, a WHEN A RUN FAILS block with Retry policy and Escalate on timeout both off, and a PUBLISH &#x26; DEPLOY section"><figcaption><p>Deploy to environment sits under Publish snapshot, with the latest version noted beneath it.</p></figcaption></figure>

#### Configuration panel

**Identity**

| Field           | Notes                                                                        |
| --------------- | ---------------------------------------------------------------------------- |
| **Name**        | Required. Empty saves are refused with *Name is required.*                   |
| **Slug**        | Required when you create the workflow, and **cannot be changed afterwards**. |
| **Description** | What it does. The agent writes one; edit freely.                             |

**Execution**

| Field                 | Notes                                                                                                                                                                     |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Execution tier**    | `Instant` (default) — synchronous, result inline, max 30s. `Standard` — asynchronous via Temporal, returns 202, max 15 min. `Long` — asynchronous, returns 202, max 36 h. |
| **Execution timeout** | A slider constrained to the tier: 1s–30s, 5s–15min, or 30s–36h. Defaults are 30s, 2.0m and 15.0m respectively.                                                            |
| **Status**            | A toggle, on by default, and shown only on workflows that already exist. Off stops new runs without deleting anything.                                                    |

**WHEN A RUN FAILS**

The **Retry policy** toggle is off by default. Its own description sets the boundary: it retries transient failures, and code errors, data errors and out-of-memory never retry. Turning it on exposes four fields.

| Field                     | Range | Default |
| ------------------------- | ----- | ------- |
| **Max attempts**          | 1–10  | 3       |
| **Initial interval (ms)** | —     | 5000    |
| **Backoff coefficient**   | —     | 2       |
| **Max interval (ms)**     | —     | 60000   |

**Escalate on timeout** is a separate toggle, off by default and hidden on the Long tier. On timeout it retries one tier up — instant → standard — and an escalated instant run returns a queued execution id to poll rather than a synchronous result.

**PUBLISH & DEPLOY** (existing workflows only)

* **Publish snapshot** — freezes the current code and configuration as a version.
* **Deploy to environment** — sends a published version to an environment.
* Underneath: a row showing the environment's current status.


# The tabs

Every tool tab in the editor: Test, Connectors, Contract, Diagram, Test cases, Executions, Sync reports, API and Docs.

#### Test

<figure><img src="/files/yp1apUmhvzKQOecdnSIY" alt="The Test tab with a ctx.input editor holding saleId, isRetry, syncMode, purchaseId and connection_id, a ctx.headers editor, and Use contract, a Live selector and Run Live above them"><figcaption><p>Until you run it, the result pane just reads Hit Run to execute without saving.</p></figcaption></figure>

Two JSON editors, `ctx.input` and `ctx.headers`. **Use contract** fills the input from the declared contract. The mode select beside the run button chooses what the run actually calls:

| Mode             | What runs                            |
| ---------------- | ------------------------------------ |
| **Live**         | Real calls to the connected systems. |
| **Partial Mock** | A mix of real calls and stubs.       |
| **Fully Mock**   | Stubs only.                          |

Then **Run Live** (or **Run**).

#### Connectors

<figure><img src="/files/XSZZ44mExCTS39KFfvfY" alt="The Connectors tab listing two extracted connectors — Cin7 Core with getSale, listCustomers and listProducts, and Trackstar with createOrder, getOrder, listOrders and more — both badged Per customer"><figcaption><p>Every action chip carries its pinned version, such as getSale V1.1.</p></figcaption></figure>

This list is **auto-extracted from the `fastn.connectors.X.Y(…)` calls in your code every time you save** — it is a readout of the code, not a separate configuration. **Add** exists for anything you need to wire manually. Each row shows a **Per customer** badge where it applies, plus the connector slug, the owning org, the action and its version.

{% hint style="warning" %}
Pinned action versions are what stop an upstream change breaking you silently. When fastn proposes a newer action, it arrives through [Pending updates](/build/connector-updates) for you to accept.
{% endhint %}

#### Contract

<figure><img src="/files/8oAjpj6kdORxuazmRS2t" alt="The Contract tab stacking an Input contract of saleId, isRetry, syncMode, purchaseId and connection_id above an Output contract of log, message, notPorted and syncStatus, each with a JSON/Schema toggle"><figcaption><p>Both editors sit on JSON here; the Schema view holds the generated contract.</p></figcaption></figure>

**Input contract** and **Output contract**, each viewable as **JSON** or **Schema**. Write an example payload in the JSON view and the schema is generated live. The schema is the contract, and is also hand-editable.

#### Diagram

Three sub-tabs.

**Flow** — a read-only React Flow rendering, generated from your code. Node kinds are `TRIGGER`, `DECISION`, `READ` and `DONE`; the controls are Zoom In, Zoom Out and Fit View. You cannot edit the graph. Edit the code and the graph follows.

**Sequence** — the same logic as an ordered list of phases.

**Docs** — generated documentation, with an **End user** / **Technical** toggle, an **Out of date** badge when the code has moved on, and **Regenerate** / **Generate**.

The end-user document has eight sections: *What this integration does*, *Before you start*, *Connected apps*, *How it works*, *Field mapping*, *Smart features*, *When it runs*, *Troubleshooting*.

{% hint style="info" %}
Generating or regenerating these docs consumes AI credits from the workspace allowance shown in the top bar.
{% endhint %}

#### Test cases

Scenarios generated during the build, organised into groups — `happy-path`, `pagination`, `fields`, `edge-cases`, `error-handling`. The header carries pass, fail and untested counters. Each row is an id such as `TC-01`, a `LIVE` or `MOCK` badge, and the expectation.

#### Executions

This workflow's run history, filtered by status pills: **All**, **200**, **201**, **400**, **404**, **422**, **500**. The workspace-wide view is [Activity → Executions](/operate/executions).

#### Sync reports

> A report appears the first time a workflow runs fastn.diff.compare.

See [Sync reports](/operate/sync-reports).

#### API

<figure><img src="/files/wXh8V6c43miEokkiUdx3" alt="The API tab with endpoint and curl"><figcaption><p>Generated from the deployment you are actually calling — copy the curl from here rather than from docs.</p></figcaption></figure>

How to call this workflow over HTTP, with a copyable curl:

```http
POST https://app.fastn.dev/api/v1/workflows/<wfId>/execute
```

Two headers steer it — `X-fastn-Test-Mode` and `x-fastn-env` — and you authenticate with an API key in the `fsk_live_…` format. Covered in full in the [HTTP API reference](/reference/api).

#### Docs

The runtime reference for the code you are writing:

| Surface           | For                                                               |
| ----------------- | ----------------------------------------------------------------- |
| `ctx.input`       | The request body, shaped by the input contract.                   |
| `ctx.headers`     | The request headers.                                              |
| `ctx.connectors`  | The connectors wired to this workflow.                            |
| `fastn.envConfig` | Per-environment values — see [Configs](/manage/configs).          |
| `fastn.unified`   | The [unified API](/build/unified-apis) surface.                   |
| `fastn.connector` | A connector call.                                                 |
| `fastn.db`        | The workspace Postgres schema — see [Database](/manage/database). |
| `fastn.state`     | Key/value state, with scopes `ORG` and `INVOCATION`.              |
| `fastn.secrets`   | Encrypted values — see [Secrets](/manage/secrets).                |

Multi-tenant calls are addressed with headers: `x-end-org-id`, `x-end-org-ref`, `x-installation-id`, `x-fastn-connections`, `x-fastn-installation-config`.

{% hint style="info" %}
This tab uses `fastn.connector` (singular) while the Connectors tab extracts from `fastn.connectors.X.Y(…)` (plural). Both spellings appear in the product; check the Docs tab in your own workspace before relying on either.
{% endhint %}

Fuller treatment in [Workflow runtime API](/reference/workflow-runtime).


# Lifecycle

How a workflow goes from draft to deployed, review gates, and code-off workspaces.

```
Create → Save (draft) → Publish (snapshot) → Deploy (to an environment)
```

| Stage       | Effect                                                               |
| ----------- | -------------------------------------------------------------------- |
| **Save**    | Persists the draft. Nothing goes live.                               |
| **Publish** | Creates v1, v2, … A snapshot, so rollback is deploying an older one. |
| **Deploy**  | The version starts handling real events in that environment.         |

An environment can be marked **Requires review**. Promoting to one of those opens a pull request on the connected GitHub repository instead of deploying straight away — the setting is per environment, and lives on [Environments](/manage/environments), which is also where the repository is connected.

### When code editing is switched off

Code editing is a per-workspace setting, and **off in almost every workspace** — it is enabled only for the parent organisation. Where it is off, a banner says so, and the editor simply has no code column: workflows are generated and updated by the AI builder, and you change behaviour by talking to the [agent](/build/agent) rather than editing the file. Everything else is unchanged — you still test, wire connectors, edit the contract, publish and deploy, and the runtime surface is identical. It can be switched on if you want to write workflow code yourself; ask fastn to enable it.


# Triggers

What starts a workflow — an inbound webhook, a schedule, or an event from a connected system.

**Integrations → Triggers** · `/integrations?tab=triggers`

<figure><img src="/files/ncfKX0bFFiclENjWAum5" alt="The Triggers page on the Schedulers tab beside Webhooks (3) and App events (0): three rows with columns Name, Tenant, Schedule showing cron expressions, Next run, Last run, Status as Disabled or Active pills, Failures, Timezone, Routes, Created and Actions"><figcaption><p>The dismissible <strong>What triggers do</strong> banner explains all three types before you pick one. The <code>Schedule</code> column shows the cron each trigger compiles to.</p></figcaption></figure>

A workflow with no trigger only runs when something calls it. A trigger is what makes it run on its own.

The page splits into three sub-tabs — **Webhooks**, **Schedulers** (`?trigger=scheduler`) and **App events** (`?trigger=app`) — with a status select offering `All statuses`, `Active` and `Disabled`.

Each list paginates, with a footer reading `Page 1 of 4 · 30 total` and Previous/Next controls. **A trigger you just created may be on a later page rather than missing** — check the count before you conclude it failed. Every table also carries a leading select-all checkbox.

{% hint style="info" %}
**One concept, several labels.** The dialog calls it **Schedule**, the sub-tab **Schedulers**, the browser title *Scheduled triggers*, and a trigger's own Type field **Scheduler**. They are all the same thing. These pages say *schedule trigger* throughout, and the list you want is the **Schedulers** tab.
{% endhint %}

### Choosing a type

<figure><img src="/files/IHLdBrz3V4gVS9C0wFau" alt="The Add a trigger dialog asking What should start the workflow?, offering Webhook, Schedule and App event as three rows, each with a sentence of explanation and a forward arrow"><figcaption><p>Each option carries its own one-line description — reproduced nowhere below, so read them here.</p></figcaption></figure>

| Type          | Reach for it when                                                             |
| ------------- | ----------------------------------------------------------------------------- |
| **Webhook**   | The sender can push, and you want it immediate.                               |
| **Schedule**  | Nightly reconciliation, hourly pulls, anything time-based.                    |
| **App event** | A HubSpot deal moves, a GitHub PR merges — and the connector supports events. |

### Inspecting a trigger

Clicking any row opens a side panel, which is where most of a trigger's life is visible. It carries **Details**, **Retry policy** and **Status history**, and its footer holds **Close**, **Trigger Now** and **Edit**.

The panel — not the row menu — is where you read *why* a trigger is in the state it is. Status history is the only place a change of status is explained, and on a schedule trigger it is also where **Consecutive Failures** and the **Re-enable Cooldown** appear.

### Managing triggers

The status select separates active from disabled. Disabling a trigger stops it firing without losing its configuration — the right move while you fix a workflow, and better than deleting it.

#### When fastn disables a trigger for you

**Disabling is not only something you do.** A schedule trigger that keeps failing is disabled by the platform, which is why the Schedulers list has a **Failures** column at all — it counts consecutive failures, and it resets when a run succeeds.

When the platform disables one, three things are true:

* The trigger's status becomes `Disabled` exactly as if you had done it by hand, so the list gives you no hint about which of you did it.
* A **Re-enable Cooldown** applies before it will run again — the scheduler detail panel states the interval.
* The reason and the moment are recorded in **Status history** on the detail panel, which keeps the last 20 status changes and marks the automatic ones.

So if a nightly job silently stops, open the row, read Status history, and look at the Failures count — do not assume someone on your team turned it off.

{% hint style="warning" %}
**How many consecutive failures trigger an auto-disable is not documented here**, because the threshold is not stated anywhere in the product. Do not design a retry strategy around a number you have guessed — confirm it with fastn if it matters to you.
{% endhint %}

### In this section

* [Webhook triggers](/build/triggers/webhooks)
* [Schedule triggers](/build/triggers/schedulers)
* [App event triggers](/build/triggers/app-events)


# Webhook triggers

A trigger that fires when another system calls a URL you give it.

A webhook trigger fires when another system calls a URL you give it. Nothing is polled.

**Columns:** `Name`, `Tenant`, `Type`, `Status`, `Auth`, `Routes`, `Created`.

* **`Auth`** reads `API key` where the webhook requires the `x-fastn-access-key` header, or `None (public)` where anyone with the URL can fire it. It is the column to scan if you are auditing exposure.
* **`Tenant`** is the customer the trigger belongs to, and reads `—` for org-level triggers.

Unlike the Schedulers and App events tables, this one ships **no `Actions` header** — the row menu is still there at the end of each row, the column simply has no title. That is a quirk of the product, not a missing feature.

### Create a webhook trigger

{% stepper %}
{% step %}

#### Name it

From **Add trigger**, choose **Webhook**.

<figure><img src="/files/xoTHjnfJFNv09F8lDm3Z" alt="The New webhook trigger dialog with Name and Description fields above a Routes section, an Add route button, and ROUTE 1 showing an unset Workflow select and Environment test (latest published)"><figcaption><p>Routes are required — a webhook with none has nowhere to send its payload.</p></figcaption></figure>

| Field           | Notes                                                      |
| --------------- | ---------------------------------------------------------- |
| **Name**        | Required. Shown in the list and on every execution record. |
| **Description** | Optional.                                                  |
| {% endstep %}   |                                                            |

{% step %}

#### Add at least one route

A route says where a payload goes when it arrives. Routes are required. Add more with **Add route** to fan a single inbound call out to several workflows.

| Field           | Notes                                                                                                                                                                                                                                                                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Workflow**    | Required. Which workflow this route runs.                                                                                                                                                                                                                                                                                                  |
| **Environment** | Optional. `test (latest published)` runs the workflow's latest published version. **Any other option is one of your org's named environments** (see [Environments](/manage/environments)) and runs the version deployed there — and **if nothing is deployed there, the fire fails**. Every org starts with one named environment, `Live`. |
| **Headers**     | Optional key/value pairs sent with the request to the workflow. Use for a key the workflow needs to call back to the sender.                                                                                                                                                                                                               |
| {% endstep %}   |                                                                                                                                                                                                                                                                                                                                            |

{% step %}

#### Set delivery attempts

How many times in total fastn tries to deliver an event to the workflow, counting the first try. Once the attempts are exhausted the delivery is recorded as failed, and you replay it from [Activity → Events](/operate/events), where every row carries a **Replay** action.

{% hint style="warning" %}
**Finding the failed one is the hard part.** Events filters by source — `All`, `Webhook`, `Scheduled`, `Manual` — and has **no status filter**, so on a busy org you cannot list failures directly. Search Events by the trigger's name and look for a row whose status is not `Delivered`. The create-trigger form calls this destination *Failed deliveries*; there is no view by that name — Activity → Events is where the events actually are.
{% endhint %}

| Field                | Range / options                                | Default     |
| -------------------- | ---------------------------------------------- | ----------- |
| **Max attempts**     | 1–10. `1` means try once and never retry.      | 3           |
| **Backoff strategy** | `Exponential (1s, 2s, 4s…)`, `Linear`, `Fixed` | Exponential |
| {% endstep %}        |                                                |             |

{% step %}

#### Set the advanced options, if you need them

<figure><img src="/files/OoaPLDukpA6WMMhV7INt" alt="The webhook dialog scrolled to Max attempts 3 and Backoff strategy Exponential, with Advanced options open on Webhook ID, Authentication API Key (x-fastn-access-key), Execution mode Parallel and Deduplication key"><figcaption><p>Advanced options start collapsed; the defaults shown here suit most senders.</p></figcaption></figure>

| Field                 | Notes                                                                                                                                                 |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Webhook ID**        | Optional. Becomes part of the public webhook URL. Leave empty and one is generated.                                                                   |
| **Authentication**    | **API Key (x-fastn-access-key)**, the default — callers must send the header. **None (public)** — anyone with the URL can fire it.                    |
| **Execution mode**    | **Parallel**, the default — concurrent events run concurrently. **Sequential** — one at a time, in arrival order.                                     |
| **Deduplication key** | Optional. A field in the incoming payload that uniquely identifies each event, so a retried delivery from the sender does not run the workflow twice. |
| {% endstep %}         |                                                                                                                                                       |

{% step %}

#### Create it, then hand out the URL

Select **Create trigger** — there is no Save button — and it joins the **Webhooks** list. Its row menu offers **Copy URL**, **Copy as cURL**, **Disable**, **Edit** and **Delete** — **Copy as cURL** is the fastest way to fire one by hand while you are debugging.
{% endstep %}
{% endstepper %}

{% hint style="danger" %}
Only use **None (public)** when the sender genuinely cannot set a header, and pair it with a deduplication key and a workflow that validates its own payload.
{% endhint %}

{% hint style="success" %}
Sequential mode plus a deduplication key is the combination that makes a webhook-driven sync idempotent. Worth setting up front rather than after the first duplicate-record incident.
{% endhint %}


# Schedule triggers

A trigger that fires on a clock you set.

A schedule trigger fires on a clock you set — nightly reconciliation, hourly pulls, anything time-based.

**Columns:** `Name`, `Tenant`, `Schedule`, `Next run`, `Last run`, `Status`, `Failures`, `Timezone`, `Routes`, `Created`, `Actions`.

Three of those are worth knowing before you scan the table:

* **`Schedule`** shows the cron expression the trigger compiles to, whichever mode built it.
* **`Failures`** counts *consecutive* failures, and resets on a success. A number climbing here is the early warning that fastn is about to disable the trigger for you — see [when fastn disables a trigger](/build/triggers#when-fastn-disables-a-trigger-for-you).
* **`Tenant`** is the customer a trigger belongs to, and reads `—` for org-level triggers.

### Create a schedule trigger

{% stepper %}
{% step %}

#### Name it

From **Add trigger**, choose **Schedule**, and give it a **Name** (and an optional **Description**).

<figure><img src="/files/l4LMcGuWfOCzr20qB0rQ" alt="The New schedule trigger dialog with Name and Description fields, mode pills Interval, Daily, Weekly, Monthly and Custom, Run every set to 5 minutes, and Timezone Asia/Karachi"><figcaption><p>Interval is the default mode, and the timezone defaults to your browser's.</p></figcaption></figure>
{% endstep %}

{% step %}

#### Pick a mode and set the schedule

Five modes:

| Mode         | Configure                                                                                               |
| ------------ | ------------------------------------------------------------------------------------------------------- |
| **Interval** | The default. **Run every** *n* + `minutes` / `hours` / `days`, with a live preview — *Every 5 minutes*. |
| **Daily**    | **Time**, defaulting to 09:00.                                                                          |
| **Weekly**   | S–M–T–W–T–F–S toggles, and a time.                                                                      |
| **Monthly**  | **Day of month**, 1–31 — one day, not several — and a time.                                             |
| **Custom**   | **Cron expression**, required. *Standard 5-field cron: minute hour day-of-month month day-of-week*      |

**Whichever mode you pick, fastn stores a cron expression** — and that is what the list's `Schedule` column shows. A trigger built with *Daily, 06:00* displays `0 6 * * *`; one built with *Interval, every 5 minutes* displays `*/5 * * * *`. The modes are a friendlier way to write the same thing, which is also why **Custom** exists: it is the escape hatch for schedules the other four cannot express.
{% endstep %}

{% step %}

#### Set the timezone and start

**Timezone** is required and defaults to your browser's zone — not your organisation's, and not the one on [your profile](/manage/profile), which only controls how times are displayed to you. Two more fields sit alongside it: **Starts at**, optional, and **Run immediately on create**, a checkbox.
{% endstep %}

{% step %}

#### Add routes, with a payload if the workflow needs one

Routes on a schedule trigger carry one field the other two do not: **Payload (JSON)**, optional, which is the body handed to the workflow when there is no inbound request to supply one.
{% endstep %}

{% step %}

#### Set the advanced options, if you need them

| Field                      | Notes                                                                                                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Scheduler ID**           | Optional. Leave it empty and one is generated.                                                                                       |
| **Delay (minutes)**        | Default `0`. Holds the run back by this many minutes after the scheduled moment — useful when an upstream system needs a head start. |
| **Keep failed deliveries** | Whether failed deliveries are retained so you can inspect and replay them, rather than discarded.                                    |
| {% endstep %}              |                                                                                                                                      |

{% step %}

#### Create it

Select **Create trigger** — there is no Save button — and it joins the **Schedulers** list. **Trigger Now** fires it immediately without waiting for the next scheduled moment; it sits in the footer of the trigger's detail panel, which opens when you click the row.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
Every time on this form is interpreted in the timezone you pick, not the viewer's. If your team is spread out, agree on one and stay with it.
{% endhint %}


# App event triggers

A trigger that fires when something changes in a connected system.

An app event trigger fires when something changes in a connected system — a HubSpot deal moves, a GitHub PR merges.

**Columns:** `Name`, `Tenant`, `Connector`, `Type`, `Events`, `Status`, `Subscription`, `Routes`, `Created`, `Actions`.

**`Subscription` is the column that matters on this page.** It reads `Subscribed` or `Failed`, and it is separate from `Status` — a trigger can be `Active` and still never fire, because *Status* is whether you have enabled it and *Subscription* is whether fastn successfully registered for events with the provider. Watch this one.

The row menu holds five items: **Copy ingest URL**, **Edit**, **Disable**, **Retry Subscription** and **Delete**.

### Create an app event trigger

The form is progressive: each answer reveals the next question. Five answers in all — name, connector, connection, event, routes — with a gate between the connector and the connection if nothing is connected yet.

{% stepper %}
{% step %}

#### Name it

From **Add trigger**, choose **App event**, and give it a **Name** (required).

<figure><img src="/files/b8U3FEEGI8btk4llael6" alt="The New app event trigger form, showing the progressive steps: name, connector, connection and event, then the routes that point at a workflow"><figcaption><p>The connector cannot be changed after the trigger is created — choose it deliberately.</p></figcaption></figure>
{% endstep %}

{% step %}

#### Choose the connector

**Connector** is required, searchable, and **immutable after the trigger is created** — choose carefully, because you cannot change it later.
{% endstep %}

{% step %}

#### Clear the connection gate

Without a live connection for that connector the form stops here: *No active connection found for this connector. Connect first to use it as a trigger source.* Make the [connection](/build/connections) first, then continue.
{% endstep %}

{% step %}

#### Pick the connection

**Connection** is required — which authorised link this trigger listens on.
{% endstep %}

{% step %}

#### Pick the event

**Event** is required: a radio list with **Search events...**, a `WEBHOOK` badge, a `Registered` or `Not Subscribed` state, the raw event key, and a **Payload Schema** expander so you can see what will arrive.
{% endstep %}

{% step %}

#### Add routes

**Routes** are required, and the same shape as a webhook's, minus the JSON payload field.
{% endstep %}

{% step %}

#### Create it, then check Subscription

Select **Create trigger** — there is no Save button — and it joins the **App events** list.

**Do not walk away at this point.** Creating the trigger asks fastn to register for events with the provider, and that registration can fail on its own, after the form has closed successfully. Look at the new row's `Subscription` column before you consider the job done.
{% endstep %}
{% endstepper %}

{% hint style="danger" %}
**`Active` with `Subscription: Failed` means the trigger will never fire.** fastn could not register for events with the provider, so nothing will ever arrive — and because `Status` still reads `Active`, the list looks healthy at a glance.

Recover with **Retry Subscription** from the row menu. If it fails again, the problem is upstream of the trigger: re-check the [connection](/build/connections) is still live and that its credential still carries the scopes the event needs.
{% endhint %}

App events are the cleanest option when the connector supports them — no URL to hand out, no polling, and fastn manages the subscription with the upstream provider on your behalf. That management is the trade: you gain a subscription you did not have to build, and you take on a failure mode the other two trigger types do not have.


# Pending updates

Vendor changes to the integrations you use, and the fixes proposed for them. Nothing is applied until you accept it.

**Integrations → Pending updates** · `/integrations/updates`

<figure><img src="/files/gEfrsUZyCGLvA5D9AiYr" alt="The Pending updates page in its empty state, reading Nothing needs your attention above a Show 1 already handled link that brings resolved proposals back into view"><figcaption><p>Nothing outstanding — the handled count is the only sign anything ever landed here.</p></figcaption></figure>

Vendors deprecate endpoints, change parameter names and alter response shapes. Normally you find out when a sync starts failing. fastn watches for these changes, works out what the fix is, and files a proposal here. The page states its own promise:

> Vendor changes to the integrations you use, and the fixes proposed for them. Nothing here is applied until you accept it.

It is a single list — there are no *Connector fixes* or *My workflows* tabs. When nothing is outstanding it reads *Nothing needs your attention…*, and a **Show N already handled** toggle brings the resolved ones back into view.

### Reading a proposal

Each card carries:

* a **status word**, such as `Applied`;
* **Integration connector:** — the connector the change affects;
* **Affected workflows:** — the workflows that depend on it;
* a description of what changed and the fix; and
* a **Details** button.

<figure><img src="/files/yffOoGm5MlnBmXn6ygHl" alt="An applied hubspot proposal tagged Fork a new major, covering 1 workflow across 3 orgs, explaining the retired Contacts Lists v1 API and three numbered migration steps, with a Notify affected orgs button"><figcaption><p>Agent confidence and provenance sit under the text; here no patch could be derived automatically.</p></figcaption></figure>

**Details** opens the proposal in full: what changed upstream, with the specific endpoints involved, and the fix set out as numbered migration steps — updated IDs, remapped parameters, changed response parsing.

### Why nothing auto-applies

Because a connector change can alter behaviour your workflows depend on. A parameter rename is safe; a response shape that drops a field your mapping reads is not. fastn does the diagnosis and the drafting, and leaves the decision with you.

{% hint style="info" %}
Version pins on a connector let you hold specific customers on the old version while you migrate the rest — remembering that a version set in code still wins over a pin. See [Connectors](/build/connectors).
{% endhint %}


# MCP gateway

Letting an AI client reach your connectors as tools.

**Connect to Claude** — in the top bar, and again under the prompt box on Home

Everything you build in fastn — connectors, actions, workflows — can be exposed to an AI client over the Model Context Protocol. The client sees your connectors as tools it can call, with the same customer scoping and the same permission model as everything else.

<figure><img src="/files/uNk5RVrtSIpkultEn3KC" alt="The Home screen, with a Connect to Claude button below the four suggestion chips and the What do you want to build? prompt box, under a time-of-day greeting"><figcaption><p>Connect to Claude sits under the build prompt on Home, below the suggestion chips.</p></figcaption></figure>

### Connecting

**Connect to Claude** opens a dialog carrying several ways in: the gateway's MCP URL, a deep link that opens Claude's custom-connector modal with it filled in, a ready-made Claude Code command, a Claude Desktop `mcp-remote` config, and an org-wide add link.

```
https://mcp.fastn.dev
```

For Claude Code, the dialog also gives you a command to run:

```
claude mcp add --transport http fastn https://mcp.fastn.dev
```

For any other MCP client, point it at that URL and authenticate with an API key from [Settings → API keys](/manage/api-keys):

```
Authorization: Bearer fsk_live_<your-key>
```

Test keys work too, with `X-fastn-Test-Mode: true` — and carry the same warning as everywhere else:

> Neither mode is a sandbox. A Test key reaches the same live connections as a Live key and causes the same real writes.

### Scoping

The gateway inherits the platform's access model rather than inventing a second one:

* **Customer scope** — a connection belongs to one customer, so a tool call runs against that customer's credential and cannot reach another's. Which customers a key may reach is set on the key itself, under *Customers it can reach*.
* **Permissions** — an API key carries a permission preset (`Full access`, `Developer`, `Operator`, `Viewer`, `End user` or `Custom`) and a per-resource matrix. That caps what the gateway can do with it.
* **Action scope** — on a [connector's](/build/connectors) detail page, the middle pane lists every action with a **Select all** control. Narrowing that selection is what makes *read-only Jira for one customer, nothing beyond that* a configuration rather than a promise.

{% hint style="warning" %}
An MCP client acts with whatever the key it holds can do. Mint a key specifically for the client, give it the narrowest permission preset that works, and name it after the client — the Name field's own helper text is *Shown in the audit log beside everything this key does.*
{% endhint %}

### Watching it

Gateway activity lands in the same places as everything else: runs appear in [Executions](/operate/executions), with the calling identity in the **Triggered by** column, and calls made with an API key are attributable to that key in the [audit log](/manage/audit-log), which is readable by Owners and Admins.

Agent usage against your AI credit allowance is broken down **By agent** in the credits popover in the top bar.

### The A2A option

The widget's Embed tab lists **A2A** — agent-to-agent — alongside Iframe and SDK, marked *soon*. That is the customer-facing counterpart: your customer's own agent reaching the integrations you offer. See [Embedding the widget](/embed/embedding).


# Common patterns

The handful of workflow shapes that come up again and again.

You can describe any of these to the [agent](/build/agent) in a sentence. They are here so you know what to ask for, and what the result should look like.

Every one of them ends up as JavaScript in a workflow's `<slug>.js`. The runtime surfaces they use — `fastn.state` (scopes `ORG` and `INVOCATION`), `fastn.db`, `fastn.secrets`, `fastn.envConfig`, `fastn.unified`, `fastn.connector` — are listed on the editor's **Docs** tab; check the exact method signatures there before copying a snippet, since they are the authority and this page is a sketch.

***

## Idempotent event sync

**The problem.** A webhook fires, the workflow writes a record. The sender retries, or someone replays the event, and you have two records.

**The shape.** Three defences, cheapest first.

1. A **deduplication key** on the [trigger](/build/triggers) — the field in the payload that identifies the event, so a retried delivery does not run the workflow twice.
2. An **idempotency guard** in the workflow, using `fastn.state`:

   ```javascript
   const seen = await fastn.state.get(`deal:${dealId}`);
   if (seen) return { success: true, skipped: true, reason: "already_processed" };
   // … do the work …
   await fastn.state.set(`deal:${dealId}`, { processedAt: new Date().toISOString() });
   ```
3. **Sequential execution mode** on the trigger, if two concurrent events for the same record could race.

**Ask for it as:** *"Make this idempotent — if the same order comes in twice, only create it once."*

***

## Change detection with a hash

**The problem.** A scheduled sync pulls 5,000 records every hour and rewrites all of them, most of which have not changed.

**The shape.** Hash the fields you care about, store the hash in `fastn.state` keyed by record id, and skip when it matches. Only changed records reach the destination.

The cost is one state read per record instead of one write to the destination, which is almost always the cheaper of the two.

**Ask for it as:** *"Only write records whose fields actually changed since the last run."*

***

## Two-stage sync for large volumes

**The problem.** A single run has to pull a large dataset and push it, and it either times out or hammers the destination's rate limit.

**The shape.** Split it.

| Stage          | Does                                                 | Tier     |
| -------------- | ---------------------------------------------------- | -------- |
| **Ingestion**  | Pulls from the source into `fastn.db`.               | Long     |
| **Publishing** | Pushes stored records to the destination in batches. | Standard |

Each stage retries independently, and a failure in publishing does not mean re-pulling everything. It also gives you a table you can query when someone asks what was actually fetched.

**Ask for it as:** *"Pull into the database first, then push in batches of 200."*

***

## Per-customer business rules

**The problem.** Every customer wants a slightly different filter — a category, a threshold, a warehouse — and you do not want a code change per customer.

**The shape.** Put the rule in configuration rather than code. The workflow reads it at runtime, so changing a customer's rule is an edit, not a deploy.

| The rule is…                                             | Put it in                                                       | Read it with          |
| -------------------------------------------------------- | --------------------------------------------------------------- | --------------------- |
| Structured, and differs per customer                     | A table in [`fastn.db`](/manage/database), with a tenant column | `fastn.db.query`      |
| Sensitive, and differs per customer                      | A [secret](/manage/secrets) scoped to that customer             | `fastn.secrets.get`   |
| The same for everyone, but differs between test and live | A [config](/manage/configs)                                     | `fastn.envConfig.get` |

Configs vary by *environment*, not by customer — so a per-customer rule belongs in the database or in a customer-scoped secret, not in a config.

{% hint style="warning" %}
`fastn.db` isolates one Postgres schema **per workspace**, not per customer. Rows are not scoped to a customer for you. If a table holds per-customer rules, put the customer on the row yourself and filter on it in every query.
{% endhint %}

Which customer a run belongs to arrives in the request headers — `x-end-org-id`, `x-end-org-ref`, `x-installation-id`, `x-fastn-connections`, `x-fastn-installation-config` — and reaches your code through `ctx.headers`. That is also how you invoke a workflow as a specific customer from outside.

**Ask for it as:** *"Make the warehouse and the minimum order value configurable per customer, not hard-coded."*

***

## Fan-out from one webhook

**The problem.** One inbound event needs to do three unrelated things — create an invoice, notify Slack, update a sheet.

**The shape.** One webhook trigger, three [routes](/build/triggers). Each route names its own workflow, and they run independently, so a Slack outage does not stop the invoice.

Prefer this over one workflow that does all three. Three workflows means three execution records, three retry policies, and a failure you can read at a glance.

**Ask for it as:** *"Fan this webhook out to three workflows."*

***

## Backfill without disturbing the live sync

**The problem.** You need to load two years of history, and the hourly sync must keep running.

**The shape.** A separate workflow on the **Long** tier, triggered manually or by API rather than by a schedule.

The guard has to be somewhere both workflows can see. A table in `fastn.db` is the safe choice: the live sync records what it has written, the backfill checks that table before writing, and neither redoes the other's work. `fastn.state` has two scopes, `ORG` and `INVOCATION` — `INVOCATION` is definitively too narrow, and if you intend to lean on `ORG` reaching across two different workflows, confirm that on the editor's Docs tab first rather than assuming it from the name.

```javascript
const done = await fastn.db.query(
  `SELECT 1 FROM synced_orders WHERE source_id = $1`, [order.id]
);
if (done.rows.length) return { skipped: true };
```

Run it against one customer first, and read the [sync report](/operate/sync-reports) before running it against everyone.

**Ask for it as:** *"A one-off backfill for the last two years, safe to run alongside the hourly sync."*

***

## Workspace-level notifications

**The problem.** You want failures posted to *your* Slack, not the customer's.

**The shape.** Use a Slack connection your organisation owns — one created from **New connection** and scoped `Account level` — rather than one belonging to a customer. On the workflow's [Connectors tab](/build/workflows) it is the row *without* a **Per customer** badge, so it behaves the same no matter whose data is being processed.

For failure notification specifically, an [alert](/operate/alerts) with a Slack destination is simpler than building it into the workflow — and it also catches the case where the workflow itself never ran.

***

## Let the platform retry before you write retry code

**The problem.** A destination is flaky, and the workflow is growing a hand-rolled retry loop.

**The shape.** Turn on **Retry policy** in the workflow's configuration panel instead. It gives you `Max attempts` (1–10, default 3), `Initial interval (ms)` (5000), `Backoff coefficient` (2) and `Max interval (ms)` (60000), applied to the run as a whole.

Know its boundary: it retries transient failures. Code errors, data errors and out-of-memory never retry, so a bug will not be papered over by attempt three.

If the failure is a timeout rather than a rejection, **Escalate on timeout** retries one tier up — instant → standard — and the escalated instant run returns a queued execution id to poll instead of a synchronous result. The toggle is hidden on the Long tier, which has nowhere further to go.

**Ask for it as:** *"Retry this three times with backoff if the destination is down."*

***

## One code path across three CRMs

**The problem.** Customer A is on HubSpot, B on Salesforce, C on Zoho, and the workflow has grown a branch per vendor for what is one operation.

**The shape.** Call the [unified API](/build/unified-apis) through `fastn.unified` instead of the individual connectors, and let fastn route to whichever provider that customer authorised. `/api/v1/unified/crm/contact` is the same call regardless of the stack underneath.

Keep the vendor-specific parts on the direct connector — the two mix freely in one workflow. Watch the entity limits: `Note`, `Channel Message` and `Direct Message` are create-only.

**Ask for it as:** *"Create the contact through the unified CRM endpoint, not through HubSpot directly."*

***

## Reconciliation

**The problem.** Everything reports success and the two systems still disagree.

**The shape.** A nightly workflow that reads counts or checksums from both sides and writes the difference to `fastn.db`, plus an [alert](/operate/alerts) on **Records failed above 0**.

It is the pattern people add after the first silent divergence. It is cheaper to add before.

**Ask for it as:** *"A nightly check that both systems have the same number of open orders, and tell me when they don't."*


# Widgets

The integrations panel your customers see inside your own product.

**Widgets**

Everything under Build exists so that this screen can do its job. The widget is the only part of fastn your customers ever touch: they browse the integrations you offer, authorise their own accounts, and configure what syncs — inside your product, under your branding.

<figure><img src="/files/xbkDHWwVrs73NZEllwSu" alt="The widget builder: a left panel with Layout, Style, Features and Embed tabs over Title and Subtitle fields, beside a tablet-width Live preview showing a purple Integrations header and a TikTok Shop row"><figcaption><p>Configure on the left, see exactly what the customer sees on the right.</p></figcaption></figure>

Two pages cover it:

{% content-ref url="<https://github.com/fastn-ai/docs/tree/main/embed/widget-builder.md>" %}
<https://github.com/fastn-ai/docs/tree/main/embed/widget-builder.md>
{% endcontent-ref %}

{% content-ref url="<https://github.com/fastn-ai/docs/tree/main/embed/embedding.md>" %}
<https://github.com/fastn-ai/docs/tree/main/embed/embedding.md>
{% endcontent-ref %}

### There is no widget list

**Widgets** in the sidebar opens `/widgets`, which *is* the builder. There is no separate list page: the **INTEGRATIONS** panel inside the builder is the list of what your widget offers. A **Live** badge appears in the builder header once the widget has been saved.

### The preview

The preview renders the widget at **Mobile**, **Tablet** and **Desktop** widths, with **Tablet** selected by default. The tenant selector at the top right, defaulting to **View as admin**, renders the widget as any one of your customers — so you can see their actual connection states rather than a mock-up. **Preview** opens it on its own.

### Saving, and the two Resets

**Save and publish** in the sticky footer pushes changes to the live widget.

{% hint style="warning" %}
The two Resets do different things. **Reset**, beside Save and publish in the footer, discards changes you have not saved. **Widget actions** ⋯ → **Reset to defaults** throws the whole configuration back to its defaults. Check which one you are clicking.
{% endhint %}


# Widget builder

Integrations, layout, style and features of the panel your customers see.

**Widgets** — the Layout, Style and Features tabs. The Embed tab has its [own page](/embed/embedding).

Opening **Widgets** takes you straight into the builder; there is no widget list page. A **Live** badge sits in the header once the widget has been saved, and **Widget actions** ⋯ offers **Reset to defaults**. The sticky footer carries **Reset** and **Save and publish**.

## Integrations

Above the tabs, the **INTEGRATIONS** panel lists what appears in the widget, with a count. This panel *is* the widget list — the offer your customers browse. Everything else on this screen is presentation.

An integration that moves data between two systems shows both logos and a direction marker, such as `Dynamics 365 F&O → BigCommerce B2B outbound →`. A status dot marks each one **draft** or **active**.

### Add Integrations

**Add** opens the **Add Integrations** dialog, with two tabs:

| Tab         | What it offers                                                  |
| ----------- | --------------------------------------------------------------- |
| **Apps**    | **Select connectors** — individual systems from your catalogue. |
| **Unified** | Unified API categories rather than one connector at a time.     |

**Scope:** decides whose credentials the integration uses.

| Scope          | Meaning                                                  |
| -------------- | -------------------------------------------------------- |
| **Org level**  | Default. Connections are shared across the organisation. |
| **User level** | Each customer connects their own account.                |

{% hint style="warning" %}
**Org level** is the default, and it is the wrong default for most embedded products: it shares one set of connections rather than giving each customer their own. If your customers are meant to authorise their own accounts, choose **User level**.
{% endhint %}

### Edit Integration

The pencil on a row opens **Edit Integration**, which is where most of the configuration actually lives.

| Field                      | Notes                                                                                       |
| -------------------------- | ------------------------------------------------------------------------------------------- |
| **NAME**                   | What the customer sees on the card.                                                         |
| **CONFIGURATION TEMPLATE** | **Edit field mappings & sync rules** — the defaults a customer starts from.                 |
| **Widget enabled**         | Switch. Off hides the integration from the widget without removing it.                      |
| **Activation mode**        | **Single activation** (default) or **Multi-connection**.                                    |
| **Customer visibility**    | **All customers** (default) or **Specific customers**.                                      |
| **Connectors**             | The connectors this integration uses.                                                       |
| **UNIFIED APIS**           | Unified entities this integration uses.                                                     |
| **Workflows**              | Bound automatically on **Save and publish**.                                                |
| **Triggers**               | Each trigger has a gear for its template defaults and for whether the end user may edit it. |
| **Trigger categories**     | Groups triggers for the customer.                                                           |

**Callbacks** send your backend an HTTP call when a customer does something in the widget. There are six:

| Callback                       | Fires when                           |
| ------------------------------ | ------------------------------------ |
| **Customer activates**         | A customer turns the integration on. |
| **Customer deactivates**       | A customer turns it off.             |
| **Customer changes settings**  | A customer edits its configuration.  |
| **Customer creates a trigger** | A customer adds a trigger.           |
| **Customer updates a trigger** | A customer edits one.                |
| **Customer deletes a trigger** | A customer removes one.              |

{% hint style="info" %}
Callbacks are how your own product learns what happened in the widget — a customer activating an integration is usually something your billing, onboarding or support tooling wants to know about.
{% endhint %}

### In this section

* [Layout](/embed/widget-builder/layout)
* [Style](/embed/widget-builder/style)
* [Features](/embed/widget-builder/features)


# Layout

The Layout tab: header content and the sections your customers see.

<figure><img src="/files/xbkDHWwVrs73NZEllwSu" alt="The Layout tab: a What you&#x27;re building explainer, an INTEGRATIONS list of 1 holding TikTok Shop with edit and delete icons, then Header content with Title set to Integrations, above Reset and Save and publish"><figcaption><p>The preview redraws as you type — that header text is exactly what appears above it.</p></figcaption></figure>

**Header content**

| Field        | Default                       |
| ------------ | ----------------------------- |
| **Title**    | `Integrations`                |
| **Subtitle** | `Connect your favorite tools` |

**Sections** toggles the blocks your customers see. All three are on by default.

| Section                 | Notes                                                            |
| ----------------------- | ---------------------------------------------------------------- |
| **Header and branding** | Your title, subtitle and accent colour across the top.           |
| **AI assistant**        | The assistant surface inside the widget.                         |
| **Search bar**          | Worth keeping once there are more than about a dozen connectors. |


# Style

The Style tab: theme, accent, type, shape and design tokens.

<figure><img src="/files/ow0MnuSi8o4FMi2YeuUm" alt="The Style tab on its Theme sub-tab, beside Accent, Type, Shape and Tokens, offering Light and Dark base cards with a note that neither is selected because custom surfaces are set"><figcaption><p>Status colours stay fixed whichever base you pick, so failed always reads as failed.</p></figcaption></figure>

Five sub-tabs: **Theme**, **Accent**, **Type**, **Shape**, **Tokens**.

### Theme

**Light** for a white product, **Dark** for a dark one. Everything except your accent colour derives from it. If you have set custom surface colours that match neither base, the UI tells you: picking one replaces them.

{% hint style="info" %}
Status colours are deliberately not themeable. A customer has to read "failed" correctly no matter what brand sits around it.
{% endhint %}

### Accent

A pair, not a single colour: **Accent** (default `#000000`) and **Text on accent** (default `#fff`), with a live contrast readout beside them. Set both — the readout is there so you can check the pairing is legible before your customers do.

### Type

| Control            | Values                                                                                        |
| ------------------ | --------------------------------------------------------------------------------------------- |
| Font               | `Inter`, `DM Sans`, `Geist`, `Plus Jakarta Sans`, `Nunito`, `Manrope`, `System UI`, `Custom…` |
| **Base size**      | 11–18px                                                                                       |
| **Font weight**    | Light, Regular, Medium                                                                        |
| **Letter spacing** | Tight, Normal, Wide                                                                           |

### Shape

Four presets — **Square**, **Rounded**, **Soft**, **Pill** — over a radius of 0–20, with separate card, button and input radii underneath if the preset is not quite right. **Shadow strength** is None, SM, MD or LG.

### Tokens

Surface colours, set individually: **Background**, **Foreground**, **Card**, **Muted**, **Muted text**, **Border**.

| Control                     | Notes                                       |
| --------------------------- | ------------------------------------------- |
| **Allowed Origins**         | Which origins may embed the widget.         |
| **Show 'Powered by fastn'** | On by default.                              |
| **Compact mode**            | Off by default.                             |
| **Design Tokens**           | `JSON` or `CSS`, plus **Import from JSON**. |

{% hint style="danger" %}
**Allowed Origins** is a security control, not a styling one. It sits on the Tokens tab, which is easy to skip. Set it to the origins that are actually allowed to embed your widget before you ship.
{% endhint %}

The **Design Tokens** block exposes fifteen variables:

`--primary`, `--primary-foreground`, `--background`, `--foreground`, `--card`, `--muted`, `--muted-foreground`, `--border`, `--radius`, `--radius-button`, `--radius-input`, `--font-family`, `--font-size`, `--font-weight`, `--letter-spacing`

**Import from JSON** takes a token set straight from your own design system, which is faster than reproducing it control by control.


# Features

The Features tab: optional capabilities your customers get inside the widget.

<figure><img src="/files/KAfwyHunHISEzVeCFdsU" alt="The Features tab with Catalog connectors and Workflow Diagram listed under Available now, both toggles off, and a Coming soon block beginning with Widget filter badged Soon"><figcaption><p>The live preview keeps rendering the customer's view while you flip these.</p></figcaption></figure>

Optional capabilities your customers get inside the widget.

**Available now** — both off by default.

| Feature                | What it does                                                                         |
| ---------------------- | ------------------------------------------------------------------------------------ |
| **Catalog connectors** | Shows all catalog connectors in the widget so end users can browse and connect them. |
| **Workflow Diagram**   | Shows the generated flow diagram on a workflow inside the widget.                    |

**Coming soon**

| Feature               | What it will do                                     |
| --------------------- | --------------------------------------------------- |
| **Widget filter**     | Let end users filter widgets by category or status. |
| **Role based access** | Role-based access control per widget.               |

{% hint style="warning" %}
**Catalog connectors** widens the offer from the integrations you curated to everything public in your catalogue. Useful for self-serve products, too broad for most. Decide deliberately.
{% endhint %}


# Embedding the widget

Three ways to put the widget in your product, and how to hand it a token safely.

**Widgets → Embed**

<figure><img src="/files/h94ScACTbG7XLRnKaC8n" alt="The Embed tab with Iframe selected beside SDK and A2A soon, a USER selector set to NewTest, and an Embed Code block with a Copy button, beside a preview reading TikTok Shop, 0 of 1 connected"><figcaption><p>The preview follows the selected user — pick one and the panel shows that customer's real connection state, not a mock-up.</p></figcaption></figure>

Pick a **USER** at the top — the widget is always scoped to one of your customers, and the selector scopes the snippet the tab generates — then choose how to mount it.

| Method     | Requirement               | Status      |
| ---------- | ------------------------- | ----------- |
| **Iframe** | Anywhere you control HTML | Available   |
| **SDK**    | You can run JavaScript    | Available   |
| **A2A**    | Agent-to-agent            | Coming soon |

## Shareable links

Instead of a snippet, send a link. It keeps working until you revoke it, and the URL carries no token.

{% hint style="warning" %}
The reference in a shareable link *is* the credential. Treat the link itself as a secret — anyone who has it can act as that customer.
{% endhint %}

Create links per customer under **Shareable links** on the Embed tab. Each row offers **Copy**, **Show** and **Revoke**.

### In this section

* [Iframe](/embed/embedding/iframe)
* [SDK](/embed/embedding/sdk)
* [Token API](/embed/embedding/token-api)


# Iframe

The simplest embed: an HTML iframe tag.

The simplest option: an HTML tag.

```html
<iframe
  src="https://YOUR_FASTN_HOST/api/v1/embed/iframe?token=YOUR_EMBED_TOKEN"
  style="width:100%; height:600px; border:none"
></iframe>
```

Use it for a CMS block, or anywhere you only control markup.

{% hint style="danger" %}
The snippet the dashboard generates contains a live token in the URL — the tab says so. That is fine for a first run and wrong for production: URLs end up in logs, referrers and browser history. For production, mint tokens from your backend and keep them out of the URL. The SDK exists for exactly that.
{% endhint %}


# SDK

The SDK mounts the same iframe and adds events, token refresh, theming and modal mode.

<figure><img src="/files/ACtyPUgDGqdvrXOLBJeV" alt="The Embed tab with SDK selected, a USER selector naming the customer the snippets are scoped to, and expandable variants starting with Connect card from @fastn-ai/embed/react and React component below it"><figcaption><p>Each variant expands to the exact snippet for the user you selected.</p></figcaption></figure>

The SDK mounts the same iframe and adds what a raw tag cannot: events when a user connects an app, token refresh, live theming, and modal mode.

Four variants, depending on how much of the UI you want to own:

| Variant               | Package path               | Use when                                                           |
| --------------------- | -------------------------- | ------------------------------------------------------------------ |
| **React component**   | `@fastn-ai/embed/react`    | Your app is React and you want the whole panel.                    |
| **Connect card**      | `@fastn-ai/embed/react`    | You want the smallest surface: one card, one button.               |
| **Script tag**        | hosted bundle, no install  | Vue, Svelte, Rails, plain HTML — anything that runs JS.            |
| **Build your own UI** | `@fastn-ai/embed/headless` | The integrations screen has to look like the rest of your product. |

### React

The confirmed exports are `FastnConnectCard`, `FastnHub` and `FastnProvider`; the headless entry point exposes the hooks `useConnectors`, `useConnections` and `useConnect`.

`FastnProvider` supplies the session the other components read, so wrap your tree in it rather than mounting `FastnHub` bare. Copy the exact props from the dashboard's generated snippet — they are rendered there for the user you selected.

### Script tag

Not an npm install: a hosted bundle you load with a `<script>` tag.

```html
<script src="https://YOUR_FASTN_HOST/api/v1/embed/assets/fastn-embed.min.js"></script>
```

```javascript
const hub = FastnEmbed.createFastnEmbed({ /* config from the Embed tab */ });

hub.mount('#hub');   // inline
hub.open();          // or as a modal

hub.on('connected', (e) => {
  // Refresh your app's data after the user connects an integration.
});
```

Because fastn hosts the screen, it gains features without you shipping a release, and its styles can never collide with yours.


# Token API

Minting and refreshing embed tokens from your backend, and the session cap.

An embed token identifies one of your customers to the widget. The dashboard shows a live 8-hour token so you can try things immediately. Mint your own from your backend for production.

### The Token API

```http
POST /api/v1/embed/token
Authorization: Bearer <API key>
x-org-id: <orgId>
```

```json
{
  "token": "emb_…",
  "endOrgId": "…",
  "role": "end_user",
  "expiresIn": 28800
}
```

`expiresIn` is 28800 seconds — eight hours.

If the API key is pinned to a particular customer, send the customer in the body instead of the header:

```json
{ "endOrgId": "…" }
```

Your API key never reaches the browser. See [API keys](/manage/api-keys).

### Refresh, and the session cap

```http
POST /api/v1/embed/token/refresh
```

{% hint style="warning" %}
Refresh is capped at **seven days per session**. At the cap the widget posts `fastn:session-expired` to the parent window and stops. Long-lived sessions do not refresh indefinitely — listen for that message and start a new session by minting a fresh token.
{% endhint %}

The flow for production:

1. Your backend authenticates the user, as it already does.
2. It calls `POST /api/v1/embed/token` with your API key and the customer identifier.
3. It returns the token to your frontend.
4. The SDK mounts with that token and refreshes it, until the seven-day session cap.


# Activity

Watching what actually happens once integrations are live.

Build tells you what should happen. Operate tells you what did.

<figure><img src="/files/QNbWUkDGunPT5Zsoug4X" alt="Activity → Events, first of the five logs: Perigon webhook rows all marked Delivered with Sep 4 timestamps, chips reading All 20, Webhook 20, Scheduled 0 and Manual 0, and Auto refresh off"><figcaption><p>Events is where a debugging pass starts — did the thing arrive at all?</p></figcaption></figure>

**Activity** holds five logs, each answering a different question.

| Page                                  | Route                    | Answers                                                       |
| ------------------------------------- | ------------------------ | ------------------------------------------------------------- |
| [Events](/operate/events)             | `/activity/events`       | What arrived, from where, and was it delivered?               |
| [Traces](/operate/traces)             | `/activity/traces`       | Which connected systems did a run call?                       |
| [Alerts](/operate/alerts)             | `/activity/alerts`       | What should tell us something is wrong, and where does it go? |
| [Executions](/operate/executions)     | `/activity/executions`   | Which runs happened, and how did each end?                    |
| [Sync reports](/operate/sync-reports) | `/activity/sync-reports` | What actually changed, record by record?                      |

Also under Operate: [**Customers**](/operate/customers) — who is using your embedded integrations. It is a top-level item in the sidebar's OPERATE group, a sibling of Activity rather than one of its logs.

And [Troubleshooting](/operate/troubleshooting), which is a page of this documentation rather than a screen in the product: something is wrong, where do I look?

### How to debug in the right order

When a customer says "it stopped working", walk down rather than around:

1. **Events** — did the event arrive at all? If not, the problem is upstream or in the trigger.
2. **Executions** — did a run start, and how did it end? Failed, Timeout and Cancelled each point somewhere different.
3. **Traces** — which external call failed or hung?
4. **Sync reports** — if the run succeeded but the data is wrong, this shows what it actually wrote.
5. **Connections** — if calls are being rejected, open the connection itself. Search for the customer rather than using the status filter chips: they currently return nothing for every state, so an empty result there tells you nothing.

{% hint style="success" %}
The step most teams skip is the first one: setting an [alert](/operate/alerts), so you reach step one before your customer does.
{% endhint %}

[Troubleshooting](/operate/troubleshooting) walks the same path organised by symptom.


# Events

Everything moving in and out of your customers' systems, newest first.

**Activity → Events**

<figure><img src="/files/QNbWUkDGunPT5Zsoug4X" alt="The Events table under columns Event, Source, Status and When: repeated Perigon article-alert webhook rows, each tagged webhook, Delivered, timestamped Sep 4, with a Replay link"><figcaption><p>Auto refresh, top-right, is off by default.</p></figcaption></figure>

An event is one inbound arrival: a webhook fired, a schedule reached, or a manual run. This is the first place to look when something did not happen.

### The table

| Column     | Notes                                                 |
| ---------- | ----------------------------------------------------- |
| **Event**  | The event identifier.                                 |
| **Source** | Where it came from — `webhook` on every row observed. |
| **Status** | `Delivered` on a successful hand-off.                 |
| **When**   | Timestamp, in your timezone.                          |
| **Replay** | Re-delivers the row. See below.                       |

Filter chips carry live counts — **All**, **Webhook**, **Scheduled**, **Manual** — so you can see at a glance whether your schedules are firing. **Search events** narrows the list.

The list is not paginated, and rows do not expand. To see what a run did with an event, go to [Executions](/operate/executions).

### Auto refresh

The toggle top-right is **off** by default. Turn it on to keep the list current without reloading — useful while you are testing a webhook from the sending system — and off again when you want to read a stable list.

### Replay

Every row has **Replay**, which re-delivers the same payload to the same workflow. Use it after fixing a workflow that failed on an event you cannot ask the sender to resend.

{% hint style="warning" %}
Replay re-runs the workflow for real. If the workflow writes to an external system, and it has no deduplication key, replaying can create duplicates. Set a [deduplication key](/build/triggers) on the webhook trigger before you need it.
{% endhint %}

An event that exhausts its delivery attempts shows a failure state in the **Status** column and can be re-sent with **Replay** once the cause is fixed. Attempt count and backoff are configured on the trigger, not here.

### When nothing is listed

An empty filter gives you **Nothing matches that filter**, with:

> Events arrive as your customers use their connected systems. Widen the filter, or check that a trigger is listening.

and a **Show all events** button. Take the second half of that literally — an empty Events log with a trigger you expected to fire is a trigger problem, not an events problem. See [Triggers](/build/triggers).

### Events versus executions

An event is *something arrived*. An execution is *a workflow ran*. One event can produce several executions when a webhook fans out to multiple routes, and an event can be Delivered while the execution it started fails. Check both.


# Traces

Every workflow run that called a connected system.

**Activity → Traces**

A trace is recorded for every workflow run that calls a connected system. Where [Executions](/operate/executions) tells you a run failed, traces are where you look for the external call behind it.

### Controls

**Search traces**, and filters for **All**, **Success**, **Error** and **Pending**.

**Pending** is the state worth watching. A trace that never resolves points at an upstream system that accepted the request and never answered — the failure mode that produces timeouts rather than errors, and the one people find hardest to diagnose.

### When the page is empty

Until a workflow calls a connected system there is nothing here. An empty filter gives you **No traces match**, with:

> A trace is recorded for every workflow run that calls a connected system.

{% hint style="info" %}
This page is thin in these docs because no trace had been recorded in the workspace we documented from. The controls above are confirmed; the shape of a populated row is not. If you are working from a workspace with real traffic, trust the screen over this page.
{% endhint %}

### Traces and timeouts

If a workflow is timing out, look here before raising its execution timeout. A longer timeout treats the symptom. Batching the calls, or moving to the Long tier, treats the cause. See [Executions](/operate/executions) for the run-level view.


# Alerts

Get told when something crosses a line you care about. Checked every 15 minutes.

**Activity → Alerts**

<figure><img src="/files/g9J2FqCpMWHtKpwNRQIu" alt="The Alerts page holding only a No alerts yet empty state, naming the destination address it would send to, with Turn on failure alerts and Custom alert buttons and New alert top-right"><figcaption><p><strong>Turn on failure alerts</strong> creates the two alerts most teams need in one click.</p></figcaption></figure>

Without an alert, a sync can fail quietly for hours. This is the cheapest insurance in the product. The empty state says as much:

> Without one, a sync can fail quietly for hours. One click turns on the two alerts most teams need…

under the heading **No alerts yet**, with **Turn on failure alerts** and **Custom alert** beside it. It names the address the two alerts would go to — the email you signed in with. **New alert** sits top-right, and the empty state is the whole page until an alert exists.

### The one-click start

**Turn on failure alerts** creates the two alerts most teams need in one click: an email the moment a run fails, and a daily reliability check. If you do nothing else on this page, do that.

### Custom alerts

<figure><img src="/files/KoPIO1YNS1RdEa7rhHbW" alt="The alert editor open on an Untitled alert badged Paused and No recipients, set to fire when Error rate is above 5% over 24 hours, watching All workflows, with Slack, Email and Webhook delivery fields"><figcaption><p>Firing history at the bottom reads This alert has not fired yet.</p></figcaption></figure>

**New alert** — or **Custom alert** on the empty state — opens the editor.

{% hint style="danger" %}
**The editor autosaves, and there is no Save button.** **New alert** persists an alert on the server the moment you click it — an `Untitled alert`, **Paused**, with **No recipients** — and every subsequent edit is saved as you make it. There is nothing to confirm and nothing to cancel. If you opened one by accident, delete it from its row; closing the editor leaves it behind.
{% endhint %}

#### Alert when

Two shapes. **a metric crosses a threshold** is the default.

* **a run fails (instant)** — fires on the failure itself.
* **a metric crosses a threshold** — fires when a measured value goes above or below a number, over a window. The default.

#### Metrics

**Error rate** is the default.

| Metric                 | Measures                                          | Unit  |
| ---------------------- | ------------------------------------------------- | ----- |
| **Error rate**         | Percent of runs that failed in the window.        | %     |
| **Success rate**       | The inverse.                                      | %     |
| **Failed runs**        | Count of failures.                                | count |
| **Total runs**         | Count of runs — catches a source that went quiet. | count |
| **Records synced**     | Volume moved.                                     | count |
| **Records failed**     | Records rejected downstream.                      | count |
| **Avg run time**       | Mean duration.                                    | time  |
| **p95 run time**       | Tail latency.                                     | time  |
| **Broken connectors**  | Connectors with failing connections.              | count |
| **Inactive workflows** | Workflows that have stopped running.              | count |

Each takes **is above** or **is below**, a value, and a window of **1 hour**, **24 hours**, **7 days** or **30 days**.

{% hint style="warning" %}
Watch the unit when you type a threshold. The default alert is **Error rate is above 5** — five *percent*. Switch the metric to **Failed runs** and the same 5 means five *runs*, which on a busy workspace is a far tighter trigger than it looks.
{% endhint %}

**Defaults on a new alert:** **a metric crosses a threshold**, metric **Error rate**, comparator **is above**, threshold **5%**, window **24 hours**.

#### Watch

A multi-select: leave it across everything, or pick the specific workflows this alert applies to. Scope noisy metrics narrowly; a global error-rate alarm on a workspace with one flaky test workflow trains you to ignore it.

#### Deliver to

| Channel     | Configure                      |
| ----------- | ------------------------------ |
| **Slack**   | An incoming webhook URL.       |
| **Email**   | One or more addresses.         |
| **Webhook** | An HTTPS endpoint of your own. |

An alert with no recipients shows **No recipients** and will not notify anyone.

#### Firing history

Each alert keeps a record of every time it fired — the fastest way to tell a real signal from a threshold set too tight. On a new one it reads `This alert has not fired yet.`

### Evaluation

Threshold alerts are evaluated every 15 minutes. Run-failure alerts are instant.

### A starting set

| Alert                                             | Why                                                   |
| ------------------------------------------------- | ----------------------------------------------------- |
| A run fails → email + Slack                       | The baseline.                                         |
| Error rate above 5% over 1 hour                   | Catches degradation before it is total.               |
| Total runs below 1 over 24 hours, on a daily sync | Catches a schedule that silently stopped.             |
| Broken connectors above 0                         | Catches expired credentials before the customer does. |


# Executions

Every workflow run, newest first.

**Activity → Executions**

<figure><img src="/files/LW2m19OFQxwyEtw3wFqM" alt="The Executions log: chips reading All 214651, Completed 212338, Failed 2298, Cancelled 15, over six Sync TikTok Orders to Cin7 Core rows, every one Failed with a Dependency Error tag"><figcaption><p>A daily 3 PM schedule failing the same way every day — the pattern the Workflow filter is for.</p></figcaption></figure>

The workspace-wide run history. Each workflow also has its own Executions tab in the editor.

### The table

| Column           | Notes                                         |
| ---------------- | --------------------------------------------- |
| **Workflow**     | Name, with a version chip.                    |
| **Tier**         | Instant, Standard or Long.                    |
| **Status**       | See below.                                    |
| **Duration**     | How long the run took.                        |
| **Triggered by** | What started it.                              |
| **When**         | Timestamp, in your timezone.                  |
| **Actions**      | The **Replay** control re-runs the execution. |

**Rows per page** is 10, 20 or 50.

### Filters

| Filter     | Values                                                                   |
| ---------- | ------------------------------------------------------------------------ |
| Date range | **All time** (default), Last 15m, 1h, 6h, 24h, 7d, 30d                   |
| Status     | **All**, Pending, Queued, Running, Completed, Failed, Timeout, Cancelled |
| Workflow   | One workflow, or all                                                     |

Alongside them: **Search executions** and **Refresh**.

| Status        | Meaning                         |
| ------------- | ------------------------------- |
| **Pending**   | Accepted, not yet queued.       |
| **Queued**    | Waiting for a runner.           |
| **Running**   | In progress.                    |
| **Completed** | Finished successfully.          |
| **Failed**    | Raised an error.                |
| **Timeout**   | Exceeded its execution timeout. |
| **Cancelled** | Stopped before finishing.       |

### Opening a run

This is where debugging actually happens. A row expands **inline** — you do not leave the page — into:

* A **result banner** for the run, and its **Raw response**.
* Chips summarising the run: **Steps**, **Success**, **Error**, **Slowest**.
* Tabs **Summary**, **Input** and **Output**.
* A footer carrying the two identifiers you need when asking anyone else about the run: `exec_…` and `wf_…`.

The Summary JSON carries the keys worth knowing by name:

| Key                                     | Tells you                                                    |
| --------------------------------------- | ------------------------------------------------------------ |
| `totalSteps`, `succeeded`, `failed`     | How much of the run got through.                             |
| `slowestStep`, `slowestMs`              | Where the time went — the first thing to read on a slow run. |
| `peakSandboxMB`, `sandboxMemoryLimitMB` | How close the run came to its memory ceiling.                |

{% hint style="warning" %}
Read `peakSandboxMB` against `sandboxMemoryLimitMB` on any run that failed without an obvious error. Out-of-memory is one of the failures a retry policy never retries, so an OOM run will not quietly recover the way a transient failure does — it just stops.
{% endhint %}

### Reading the statuses

**Failed** is a workflow error — bad data, a rejected call, a bug. Expand the row for the result banner and raw response, then check [Traces](/operate/traces) for the external call behind it.

**Timeout** means the tier's budget ran out. Either the work genuinely needs longer, or a single external call is hanging. Check [Traces](/operate/traces) before raising the timeout.

**Queued** for an extended period means the run has been accepted but has not yet reached a runner.

**Cancelled** means the run stopped before finishing.

### The per-workflow tab

The **Executions** tab inside a workflow's editor filters differently: its pills are HTTP status codes — **All**, `200`, `201`, `400`, `404`, `422`, `500` — rather than run statuses. Use this page when you want to know *how a run ended*, and that tab when you want to know *what the caller got back*.


# Sync reports

What changed on the last run of each sync, record by record.

**Activity → Sync reports**

Executions tell you a run succeeded. Sync reports tell you what it did, record by record.

### How a report is produced

> A report appears the first time a workflow runs `fastn.diff.compare`.

Which implies reports are effectively opt-in at the workflow level: a workflow that compares its source and target through `fastn.diff.compare` produces one, and a workflow that does not, does not.

### What it is for

The gap between "the run succeeded" and "the data is right" is where support tickets live. When a customer says records are missing, this is the page that distinguishes *the record was filtered out* from *the record was never seen* — which the executions log cannot tell you.

Each workflow also has a **Sync reports** tab in its editor, scoped to that workflow, with the same empty-state note.

### Controls

**Search reports**, and nothing else. Unlike Events, Traces and Executions, this page has no status filters and no date range.

Until a workflow has run `fastn.diff.compare`, the page reads **No sync reports yet**.

{% hint style="info" %}
This page is thin in these docs because no sync report had been produced in the workspace we documented from — the shape of a populated report is not something we can describe accurately. If you are building a data sync and this page stays empty, ask the agent to add diff reporting. It is far easier than reconstructing what a run did from logs afterwards.
{% endhint %}


# Customers

Every customer using your embedded integrations.

**Customers** · `/settings/customers`

A top-level item in the sidebar's OPERATE group, beside Activity rather than inside it. Note the mismatch between where it sits and where it lives: the sidebar puts it under OPERATE, but its route is under `/settings/`. Linking to `/customers` gets you a **Page not found**.

<figure><img src="/files/pCUS3D9bx04vwI4jgkgz" alt="The Customers table with a single row: testing, identifier testing underneath, 1 connection, status Active, ending in View connections — with an All tiers filter and a 1 customer count above"><figcaption><p>The second line under each name is the identifier you will see again as endOrgId.</p></figcaption></figure>

A customer is one of your customers — a container for their connections, credentials and workflow data.

### The table

| Column          | Notes                                         |
| --------------- | --------------------------------------------- |
| **Customer**    | Display name, with the identifier underneath. |
| **Connections** | How many systems they have authorised.        |
| **Status**      | Active, or Pending admin activation.          |

A count sits above the table, and **Search customers** narrows it. **View connections** on each row filters [Connections](/build/connections) to that customer.

### Status

**Active** — set up and usable.

**Pending admin activation** — created, but an administrator has not activated it. Its integrations will not run until they do.

### Creating a customer

**Create customer** adds one manually, which is what you want when onboarding an account before they first sign in.

The identifier is the stable one — use your own account ID rather than a display name. It is what you will see again as `endOrgId` in the [embed token](/embed/embedding) response, and as the final `tenant` segment of a connection ID.

### Customers and tenants

The product uses both words. **Tenant** is the column header on all three Triggers tables, and it is the last segment of the connection ID format:

```
ucl:org_<org>:<env>:<connectorId>:<authId>:<tenant>
```

Treat them as the same thing: one isolated customer container, called a tenant wherever the plumbing shows through.

### Where customers show up elsewhere

| Surface                           | What it does                                                |
| --------------------------------- | ----------------------------------------------------------- |
| ⌘K search, **CUSTOMERS** group    | Jump straight to one.                                       |
| [Connections](/build/connections) | A **Customer** column on every connection.                  |
| Widget builder preview            | The tenant selector renders the widget as a customer.       |
| [Embed tab](/embed/embedding)     | The **USER** selector scopes the generated snippet.         |
| Triggers tables                   | A **Tenant** column on webhooks, schedulers and app events. |


# Troubleshooting

Symptom, where to look, and what usually fixes it.

Work down this page in order. Most problems resolve at the first or second step, and each step tells you which screen holds the answer.

### Start here

| Symptom                                  | Look first at                                                                       |
| ---------------------------------------- | ----------------------------------------------------------------------------------- |
| Nothing happened at all                  | [Events](/operate/events) — did anything arrive?                                    |
| Something ran but ended badly            | [Executions](/operate/executions) — what status?                                    |
| It succeeded but the data is wrong       | [Sync reports](/operate/sync-reports)                                               |
| It is slow                               | [Traces](/operate/traces)                                                           |
| It worked yesterday and not today        | [Connections](/build/connections), then [Pending updates](/build/connector-updates) |
| One customer is affected, others are not | [Connections](/build/connections), scoped to that customer                          |

***

## Nothing ran

### No event in the log

If [Events](/operate/events) shows nothing, the problem is before fastn.

| Trigger type  | Check                                                                                                                          |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Webhook**   | Is the sending system pointed at the right URL? Does it require the `x-fastn-access-key` header, and is the sender sending it? |
| **Schedule**  | Is the trigger Active rather than Disabled? Is its timezone what you assumed?                                                  |
| **App event** | Is the connector's **Webhook config** set up, and does the customer have an active connection?                                 |

Filter the Events chips by **Scheduled** — a count of zero on a schedule you expected to fire is the answer.

### Event Delivered, but no execution

A Delivered event means the payload arrived and was handed on. If no execution followed:

* **The workflow was never published.** Every call returns `WORKFLOW_NOT_PUBLISHED` until a snapshot is published, so nothing runs and nothing appears in Executions. The workflows list shows this as **Not published** / **Unpublished**, and its tooltip says so: "Never published — this workflow cannot run yet." This is the likeliest cause of *nothing ran at all*. Publish a snapshot from the editor.
* The workflow is **disabled**. Check the Status toggle in its Configuration panel.
* The trigger route points at an environment with **nothing deployed to it**. A named environment runs the version deployed there, and the fire fails if nothing is. Either deploy, or point the route at `test`, which runs the latest published version.

### Event never Delivered

It exhausted its delivery attempts. The row shows a failure state in the **Status** column; fix the cause, then **Replay** it. Attempt count and backoff are on the webhook trigger — see [Triggers](/build/triggers).

***

## It ran and failed

### Status: Failed

Expand the execution row for its result banner and raw response, then check [Traces](/operate/traces) for the call that was rejected. The usual causes, in order of frequency:

1. **The connection is Expired or Failed.** The customer re-authorises through your widget.
2. **The upstream API changed.** Check [Pending updates](/build/connector-updates) for a proposal against that connector.
3. **The data did not match the contract.** A field the mapping expects is absent or the wrong type.
4. **A permission is missing.** The customer's OAuth grant may not cover the scope the action needs.

### Status: Timeout

The tier's budget ran out. Before raising the timeout, open [Traces](/operate/traces):

* **One slow call** — the fix is upstream, or batching, not a longer timeout.
* **Hundreds of calls** — batch them, or move to the Long tier.
* **A Pending trace that never resolved** — the upstream system accepted the request and never answered.

**Escalate on timeout** retries one tier up — instant to standard — which buys time without redesigning. It returns a queued execution id to poll, so the caller's response shape changes; the option is hidden on the Long tier, which has nothing above it.

### Status: Failed with no obvious error

Expand the row and read `peakSandboxMB` against `sandboxMemoryLimitMB`. Out-of-memory never retries, so a run that hit the ceiling simply stops rather than recovering on the next attempt.

### Retries

The retry policy retries transient failures. Code errors, data errors and out-of-memory never retry, however many attempts you allow — so if the same run fails identically every time, the policy is not the thing to adjust.

***

## It succeeded but the data is wrong

Open [Sync reports](/operate/sync-reports). It shows what the run did, record by record — which distinguishes *the record was filtered out* from *the record was never seen*.

If the page is empty, the workflow does not call `fastn.diff.compare`. Ask the agent to add diff reporting; reconstructing this from logs afterwards is far harder.

Common causes:

| Cause                    | Where to fix it                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------ |
| A field was never mapped | The workflow's field mappings — tell the agent what is missing.                      |
| A filter is too broad    | The include/exclude rules in the workflow.                                           |
| Duplicates downstream    | No deduplication key on the trigger, or no `fastn.state` idempotency guard.          |
| Stale values             | A pinned connector version that predates a field. See Version pins on the connector. |

***

## Duplicates after a replay

Replay re-runs the workflow for real. Without protection it will write twice.

Two defences, and you want both:

1. A **deduplication key** on the webhook trigger — a field that uniquely identifies each event, so a retried delivery from the sender does not run the workflow twice.
2. An **idempotency guard** in the workflow using `fastn.state`:

```javascript
const seen = await fastn.state.get(`deal:${dealId}`);
if (seen) return { success: true, skipped: true, reason: "already_processed" };
```

See [Workflow runtime API](/reference/workflow-runtime).

***

## It works for one customer and not another

Almost always a connection. Open [Connections](/build/connections) — the Customer column tells you whose each one is, and the search box narrows the list.

| Status       | Fix                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------- |
| **Expired**  | The customer re-authorises through your widget.                                             |
| **Failed**   | Access was revoked, a password changed, or a key was rotated. Same fix.                     |
| **Inactive** | The row menu offers **Reconnect** and **Disconnect**. There is no re-enable — reconnect it. |
| Missing      | They never connected that system.                                                           |

Use the search box rather than the status filter chips: see [Known issues](#known-issues) below.

Also check whether that customer is **pinned to an older connector version** — see Version pins on the connector.

***

## API calls are rejected

| Response                 | Cause                                                                                                                                                                                                               |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WORKFLOW_NOT_PUBLISHED` | No snapshot has been published for that workflow. Publish one from the editor.                                                                                                                                      |
| Rejected with a test key | Missing `X-fastn-Test-Mode: true`. A test key is refused without it.                                                                                                                                                |
| Wrong code ran           | `x-fastn-env`. `test` means the latest published version; any other slug means the version deployed there.                                                                                                          |
| Forbidden                | The key's permissions do not cover the call. Key permissions are a preset plus a **What it can touch** matrix on the key itself — see [API keys](/manage/api-keys). [Roles](/manage/roles) govern people, not keys. |

***

## You deleted something by mistake

Connectors, connector actions and workflows go to [Trash](/manage/trash) and restore with slug and history intact. Other resources — as the product puts it, "widgets and their integrations among them" — are deleted immediately and cannot be restored from that page.

***

## Known issues

These are current product defects rather than anything you have misconfigured. Route around them.

| What you see                                                                             | What is actually happening                                                                                                 |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Connections filter chips (**Active**/**Inactive**/**Expired**/**Failed**) return nothing | The chips report 0 for every state even when rows are plainly Active. Search by customer or connector instead.             |
| A connector shows **Connected** in the list but **0 connections** on its detail page     | The list badge and the detail count disagree. Check [Connections](/build/connections) for the real answer.                 |
| `Created: Invalid Date` on a connector overview                                          | A date-rendering bug, not a broken connector.                                                                              |
| Trash → **Actions** hangs on `Loading deleted actions…`                                  | The tab never resolves. Deleted actions cannot currently be reviewed there.                                                |
| The connector catalogue count is higher than the systems you recognise                   | Several systems appear twice, once **managed** and once **Custom**. The headline count is not a count of distinct systems. |

***

## Making the next one easier

| Do this once                                            | Saves you                           |
| ------------------------------------------------------- | ----------------------------------- |
| **Turn on failure alerts** on [Alerts](/operate/alerts) | Finding out from a customer         |
| Add an alert on **Broken connectors above 0**           | Expired credentials going unnoticed |
| Add an alert on **Total runs below 1 over 24 hours**    | A schedule that silently stopped    |
| Set a **deduplication key** on every webhook trigger    | Duplicate records after any replay  |
| Add `fastn.diff.compare` to every sync                  | Reconstructing a run from logs      |


# Settings

Organisation settings, access, credentials and billing.

**Settings** in the MANAGE group of the left rail.

**Its sub-navigation is role-scoped — you will not see every page below.** An Owner or Admin sees People, General, API keys, Secrets, Environments, Configs, Database, SaaS Connectors, Billing, Roles, Audit log and Trash; a Developer sees API keys, Secrets, Environments, Configs, Database and Trash. The pages a Developer does not get are the administrative ones — People, General, Billing, Roles and the Audit log; Trash shows for everyone.

The table follows the Owner sidebar order, with Trash last.

| Page                                            | Covers                                                   |
| ----------------------------------------------- | -------------------------------------------------------- |
| [People](/manage/people)                        | Who is in the workspace, invitations, statuses.          |
| [General](/manage/general)                      | Organisation name, timezone, joining by email domain.    |
| [API keys](/manage/api-keys)                    | Programmatic access, test and live modes.                |
| [Secrets](/manage/secrets)                      | Encrypted values read at runtime.                        |
| [Environments and GitHub](/manage/environments) | Deployment stages and review gates.                      |
| [Configs](/manage/configs)                      | Per-environment values read at runtime.                  |
| [Database](/manage/database)                    | Which Postgres your workflows read and write through.    |
| [SaaS Connectors](/manage/saas-connectors)      | Your own SaaS API, and the scopes tenants connect under. |
| [Billing and limits](/manage/billing)           | Plan, credits and limits.                                |
| [Roles](/manage/roles)                          | What each role can do, and how to narrow one.            |
| [Audit log](/manage/audit-log)                  | Every action taken in the organisation.                  |
| [Trash](/manage/trash)                          | Deleted connectors, actions and workflows.               |

**Customers** is not here — it is a top-level item in the OPERATE group, and is documented with [Operate](/operate/customers).

Your own account is separate from the organisation:

{% content-ref url="/pages/ABxxcQtMkrSaTuhRvHMp" %}
[Your profile](/manage/profile)
{% endcontent-ref %}

### Who can reach what

The sidebar itself changes by role, so the first answer to "why can't I see that page" is usually the role you are signed in as.

| Page                                      | Who reaches it                                                                   |
| ----------------------------------------- | -------------------------------------------------------------------------------- |
| Audit log                                 | Owner or Admin — gated by role, enforced at the API layer on `/api/v1/audit-log` |
| People, General, Billing, Roles           | In the Owner and Admin sidebar; absent from a Developer's.                       |
| Secrets                                   | The `Secrets` permissions — `read`, `write`, `delete`.                           |
| API keys, Environments, Configs, Database | In both the Owner/Admin and Developer sidebars.                                  |
| Trash                                     | In the Owner, Admin and Developer sidebars, at `/settings/trash`.                |
| Everything else                           | Per the permission matrix in [Roles](/manage/roles).                             |


# People

Who can see and change things in this workspace.

**Settings → People**

<figure><img src="/files/m9DwtdFBJ5boqU4GYcoE" alt="The People list with four rows, names and addresses obscured — three Admins and one Owner, all Active with no extra Access, under the chips Everyone 4, Owner 1 and Admin 3, each row ending in Change role and a delete control"><figcaption><p>Change role is greyed out on the Owner's own row.</p></figcaption></figure>

| Column     | Notes                                                              |
| ---------- | ------------------------------------------------------------------ |
| **Person** | Name and email.                                                    |
| **Role**   | Their assigned role. **Change role** edits it.                     |
| **Status** | **Active** in every observed row. See [Statuses](#statuses) below. |
| **Added**  | When they joined.                                                  |
| **Access** | Any scoping beyond the role. It showed `—` in every observed row.  |

**Search by name or email** narrows the list. Filter chips carry a count per role — Everyone, Owner, Admin and so on — and the dropdown on the right filters by status. Each row carries a checkbox; no bulk-action control was visible alongside them.

### Inviting someone

**Invite someone** sends an invitation to an email address with a role attached. Until they accept, they appear with status **Invited**.

If you have set up [domain joining](/manage/general), people on that domain can request access instead of waiting for an invite, and the role you configured there is preselected when you invite them.

### Changing a role

**Change role** on any row. What you may assign is bounded by your own role — you cannot grant more than you hold.

**Change role** is disabled on the Owner's own row, so the Owner's role cannot be reassigned from this screen. There is exactly one Owner per organisation.

### Removing someone

The trash icon removes a person from the workspace. Their audit-log history stays, so *who did what* remains answerable after they leave.

{% hint style="info" %}
API keys are workspace credentials rather than personal ones, so do not assume that removing a person revokes the keys they created. Confirm on [API keys](/manage/api-keys) and rotate anything they had access to.
{% endhint %}

### Statuses

**Active** is the only status observed in the product. The rest are the values the filter dropdown offers; treat their exact meanings as provisional until you have seen one.

| Status        | Meaning                                           |
| ------------- | ------------------------------------------------- |
| **Active**    | Signed up and usable. Confirmed.                  |
| **Invited**   | Invitation sent, not yet accepted. Provisional.   |
| **Disabled**  | Cannot sign in. Provisional.                      |
| **Suspended** | Access withdrawn pending a decision. Provisional. |

What each role can actually do is on the next page.

{% content-ref url="/pages/77QHtoGMf0SrvnYOP9yw" %}
[Roles](/manage/roles)
{% endcontent-ref %}


# Roles

What each role can do, and how to narrow one.

**Settings → Roles**

<figure><img src="/files/BAKgL7MnS2sKpqCycDBk" alt="The Roles screen with Owner 42, Admin 42, Developer 34, Operator 21, Viewer 6 and End User 35 down the left, Admin selected at 42 of 42 permissions and 100%, and its Connectors 5 of 5, Connections 6 of 6 and Workflows 7 of 7 groups fully ticked"><figcaption><p>Two capabilities are called out as role-gated rather than grantable: using the AI assistant and reading the audit log.</p></figcaption></figure>

> A custom role narrows a built-in one; assign either to a person on the People screen, or to an embed tier under Billing.

### The six built-in roles

| Role          | Permissions | Summary                                                                                                                  |
| ------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Owner**     | 42          | One per organisation. Manages admins and transfers ownership.                                                            |
| **Admin**     | 42          | Full access within their organisation and customers.                                                                     |
| **Developer** | 34          | Builds and deploys connectors, workflows and agents.                                                                     |
| **Operator**  | 21          | Configures connections and runs workflows. Deploys to test only.                                                         |
| **Viewer**    | 6           | Read-only on connectors, connections, workflows, agents, widgets and events. Not executions, users, settings or secrets. |
| **End User**  | 35          | Uses the embed widget: creates connections, runs agents.                                                                 |

Selecting a role on the left shows its permission matrix, the count it holds out of 42, and a percentage bar. Built-in roles carry a **Built in** badge.

### Platform Admin

A seventh role, **Platform Admin**, appears in the organisation switcher rather than on this screen. It is platform-level — held by fastn, not something you assign to someone inside your own organisation — and it is what publishes connectors to the public catalogue.

### Roles here are not API-key permissions

The six roles on this screen govern **people**. API keys carry their own permission presets — `Full access`, `Developer`, `Operator`, `Viewer` (the default), `End user` and `Custom` — chosen on the key creation dialog. The names overlap, but they are two separate systems configured in two different places; granting a person the Developer role does nothing to a key, and vice versa. See [API keys](/manage/api-keys).

### The permission matrix

| Resource        | Permissions                                                     |
| --------------- | --------------------------------------------------------------- |
| **Connectors**  | create, read, update, delete, write                             |
| **Connections** | create, read, update, delete, share, decrypt                    |
| **Workflows**   | create, read, update, delete, execute, deploy test, deploy prod |
| **Users**       | create, read, update, invite, remove                            |
| **Settings**    | read, update, manage                                            |
| **Events**      | create, read, update, delete, replay                            |
| **Secrets**     | read, write, delete                                             |
| **Executions**  | create, read                                                    |
| **Widgets**     | create, read, update, delete                                    |
| **Unified API** | read, execute                                                   |

### Two capabilities that are not permissions

Two things are gated by role directly rather than by a grantable permission, and cannot be turned on or off per user:

* **Use the AI assistant** — the agent service checks the caller's role.
* **Read the audit log** — `/api/v1/audit-log` is restricted to account owners and admins.

Both are called out in a banner at the top of any role that has them.

### Custom roles

**Custom role**, or **Duplicate** on a built-in, creates a role that *narrows* its parent. You can take permissions away; you cannot add ones the parent does not have.

Assign a custom role to a person on [People](/manage/people), or — as the line at the top of this screen puts it — to an embed tier under [Billing](/manage/billing), which is how you scope what an embedded end user may do.

### The boundary worth knowing

**Operator** is described on this screen as deploying **to test only**. Whether **Developer** holds `deploy prod` is the difference that usually draws the line between people who build and people who run — check the Developer matrix on this screen before you rely on it either way.

`decrypt` on Connections and `read` on Secrets are the two most sensitive permissions in the matrix. Viewer holds neither. Operator's matrix is worth reading directly rather than assumed.


# General

Your organisation and how people join it.

**Settings → General**

<figure><img src="/files/FpreBsAbBISaflSmSZZJ" alt="General settings: an Organisation block holding the organisation Name and Timezone Asia/Karachi, then Joining by email domain with an empty Email domain field, Approve automatically off, and the joining role set to Developer"><figcaption><p>The role selector only preselects what a joiner gets; it is not a ceiling.</p></figcaption></figure>

### Organisation

| Field        | Notes                                                                                        |
| ------------ | -------------------------------------------------------------------------------------------- |
| **Name**     | Shown to your team and on invitations.                                                       |
| **Timezone** | Sets the default timezone for new schedules. Dates elsewhere are shown in your own timezone. |

{% hint style="info" %}
The timezone here is the default offered when creating a [schedule trigger](/build/triggers), not a global rewrite. A schedule keeps whatever timezone it was saved with.
{% endhint %}

### Joining by email domain

Anyone signing up with this domain can ask to join instead of waiting for an invite.

| Field                                      | Notes                                                                                                                                                                                                                                                        |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Email domain \***                        | Required. Placeholder `yourcompany.com`. Must be a domain your organisation controls, not a mailbox provider.                                                                                                                                                |
| **Approve automatically**                  | Off means an admin approves each request. Leave it off unless the domain is yours alone.                                                                                                                                                                     |
| **Role people joining on this domain get** | A default role selector. Which roles it offers was not captured with the dropdown open — open it and check rather than assuming. Whatever it is set to is a starting point, not a limit: you can still invite someone as anything you are allowed to assign. |

{% hint style="warning" %}
Never set a mailbox provider here. Auto-approve on a shared domain means anyone with an address there can join your workspace.
{% endhint %}

### Saving

The two sections above are what this page is documented to hold. Anything further down it — including how edits are committed — has not been captured, so save and discard the way the screen tells you to rather than the way this page describes.


# API keys

Programmatic access to your workspace.

**Settings → API keys**

<figure><img src="/files/aWa3xYnaWWRhMKblPmIF" alt="The API keys list with four rows — Workflow, two called MCP, and TEST — every one badged Live, three Full access and one Default, each offering Rotate; the filters read All 4, Live 4, Test 0"><figcaption><p>The Key column shows only a prefix and the last four characters, with a copy icon.</p></figcaption></figure>

| Column        | Notes                                              |
| ------------- | -------------------------------------------------- |
| **Name**      | What you called it.                                |
| **Key**       | Prefix and last four characters, with a copy icon. |
| **Mode**      | Live or Test.                                      |
| **Scope**     | The permissions it carries.                        |
| **Last used** | Timestamp, or Never.                               |

**Search keys** narrows the list, and the filters are **All** / **Live** / **Test**.

**Rotate** issues a new secret for the same key. The **…** menu holds **Edit**, **Identity verification** and **Revoke** — there is no delete. Revoking is the way a key ends.

### Creating a key

<figure><img src="/files/x31FvP0Mrb8Hz0JIAXk2" alt="The Create an API key dialog with a Name field, Mode cards Test selected and Live carrying a warning, Permissions set to Viewer, and a line saying the key reads test data and expires in 90 days"><figcaption><p>The dialog spells out that Test is a separate credential, not a sandbox.</p></figcaption></figure>

The dialog runs in three sections: **Identity**, **Access** and **Limits**.

#### Identity

**Name \*** — shown in the audit log beside everything this key does. Name it after the system that will hold it, not after a person.

**Mode** — **Test** or **Live**, defaulting to **Test**.

| Mode     | Behaviour                                                                            |
| -------- | ------------------------------------------------------------------------------------ |
| **Test** | Separately revocable, and refused unless the caller sends `X-fastn-Test-Mode: true`. |
| **Live** | Acts on real customer data. Treat it like a password.                                |

{% hint style="danger" %}
Neither mode is a sandbox. A Test key reaches the same live connections as a Live key and causes the same real writes — it is a separate credential, not a safe one.
{% endhint %}

#### Access

**Permissions \*** takes one of six presets — **Full access**, **Developer**, **Operator**, **Viewer** (the default), **End user** or **Custom**. These are key presets, not the people roles on [Roles](/manage/roles), even where the names match. Choose the narrowest that works: a key that only reads should stay on Viewer.

Whichever preset you pick, the **What it can touch** matrix below shows exactly what it grants, and **Custom** lets you set each box yourself. Ten resource groups:

| Resource        | Permissions                                                       |
| --------------- | ----------------------------------------------------------------- |
| **Connectors**  | create, read, update, delete, write                               |
| **Connections** | create, read, update, delete, share, decrypt                      |
| **Workflows**   | create, read, update, delete, execute, deploy\_test, deploy\_prod |
| **Users**       | create, read, update, invite, remove                              |
| **Settings**    | read, update, manage                                              |
| **Events**      | create, read, update, delete, replay                              |
| **Secrets**     | read, write, delete                                               |
| **Executions**  | create, read                                                      |
| **Widgets**     | create, read, update, delete                                      |
| **Unified API** | create, read, update, delete, execute                             |

{% hint style="warning" %}
`decrypt` on Connections and `read` on Secrets are the two boxes worth arguing about before you tick them. A key holding either can read credentials, and a key is easier to leak than a person.
{% endhint %}

**Customers it can reach** — **Every customer**, or **Only the ones I pick**. Narrowing it is not optional everywhere: the form notes that picking specific customers is *Required for keys used in an embed session.*

**Verify caller identity (HMAC)** — turning it on requires callers to send a signed `x-identity-hmac` header, so a caller cannot change the `x-org-id` or `x-user-id` it claims to be. The signing secret is shown once at creation. If you lose it, reach it again through **Identity verification** on the key's **…** menu.

**Resource scope** *(optional)* — narrows the key to particular resources rather than everything its permissions would otherwise cover.

#### Limits

| Field                                 | Notes                                                                                              |
| ------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Allowed IP addresses** *(optional)* | An allowlist. Left blank, the key works from any address.                                          |
| **Rate limit** *(optional)*           | A ceiling per minute for this key.                                                                 |
| **Expires \***                        | **In 30 days**, **In 90 days** (the default), **In a year**, **Never**, or **On a specific date**. |

The dialog keeps a live summary sentence at the bottom describing what you have built, so you can read back the whole key before committing:

> This key can read on test data for every customer, from any address, and expires in 90 days.

### Using a key

```bash
curl -X POST https://YOUR_FASTN_HOST/api/v1/workflows/WORKFLOW_ID/execute \
  -H "Authorization: Bearer fsk_live_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "key": "value" } }'
```

With a test key, add the two headers:

```bash
  -H "Authorization: Bearer fsk_test_<your-key>" \
  -H "X-fastn-Test-Mode: true" \
  -H "x-fastn-env: test"
```

Full detail in the [HTTP API reference](/reference/api).

### Practice worth keeping

* One key per consuming system, so revoking one does not take down four things.
* Rotate on a schedule, and when anyone with access leaves.
* Never in client-side code. Browser-facing widgets use short-lived embed tokens minted by your backend — see [Embedding the widget](/embed/embedding).
* Check **Last used** before revoking. A key showing Never is either unused or misconfigured; either way, find out which.
* Set an expiry rather than **Never**, and turn on HMAC verification for any key a customer's browser session depends on.

{% hint style="info" %}
**API keys per customer** is one of the limits listed under [Billing](/manage/billing). Check there if key creation starts refusing.
{% endhint %}


# Secrets

Encrypted values your workflows read at runtime.

**Settings → Secrets**

> Encrypted values your workflows read at runtime. Scope a secret to a customer or environment for per-tenant overrides.

<figure><img src="/files/N7rjf4gtbjrgiTF8BFlp" alt="The Create Secret side panel: Name placeholdered STRIPE_SECRET_KEY above the note UPPER_SNAKE_CASE used as the argument to fastn.secrets.get(), Type set to Text, an empty Value box, Customer set to All customers (org-wide), and Cancel and Create Secret in the footer"><figcaption><p>The panel scrolls past what is shown here — an <strong>Environment</strong> selector follows <strong>Customer</strong>.</p></figcaption></figure>

A secret is written once and never shown again, so a key never has to live in your code:

```javascript
const apiToken = await fastn.secrets.get("SHOPIFY_API_TOKEN");
```

With no secrets yet, the page shows **No secrets yet** above the same instruction:

> A secret is written once and never shown again. Workflows read it with fastn.secrets.get.

### The list

Once secrets exist, the page is a table:

| Column                    | Holds                                                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Name**                  | The UPPER\_SNAKE\_CASE name, which is what your code passes to `fastn.secrets.get()`.                               |
| **Type**                  | `Text` or `JSON`.                                                                                                   |
| **Scope**                 | Where the value applies — `org` for an org-wide default, otherwise the customer and/or environment it is pinned to. |
| **Created** / **Updated** | Dates. Overwriting a value moves **Updated**, since a secret is replaced rather than versioned.                     |

Each row ends in **Edit** and a delete control. The value itself is never shown in the table — only its metadata.

### Creating one

**New secret** opens the **Create Secret** side panel, which has five fields. The panel scrolls — **Environment** sits below **Customer**, past the fold.

| Field           | Notes                                                                                                                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name \***     | UPPER\_SNAKE\_CASE. This string is literally the argument to `fastn.secrets.get()`, so `SHOPIFY_API_TOKEN` here is `fastn.secrets.get("SHOPIFY_API_TOKEN")` in code.                             |
| **Type**        | **Text** (the default) or **JSON**. The panel states the difference: *"A JSON secret is validated on save and `fastn.secrets.get()` returns it parsed; a text secret returns the exact string."* |
| **Value \***    | The value itself. Written once; the screen does not show it again afterwards.                                                                                                                    |
| **Customer**    | Defaults to **All customers (org-wide)**; the dropdown lists your customers. *"Scope this secret to a specific customer. Leave blank for an org-wide default that applies to all customers."*    |
| **Environment** | Defaults to **All environments**; the other choices are **test** and **Live**. *"Scope this secret to a specific environment. Leave blank for an org-wide default."*                             |

To change a value, write a new one over the same name.

### Scoping

A secret can be org-wide, or scoped to a **customer**, an **environment**, or both — which is what gives you per-tenant overrides without branching in code. The same `fastn.secrets.get("PARTNER_TOKEN")` call is what runs for everybody.

{% hint style="info" %}
How fastn picks between a customer-scoped and an environment-scoped value when both could match is not documented here. If you rely on overlapping scopes, set one up and confirm which value a run actually reads before you build on it.
{% endhint %}

### What belongs here

Third-party API tokens, database credentials, signing keys, webhook signing secrets — anything you would not paste into a ticket.

What does **not** belong here: connector credentials. Those live on [connections](/build/connections) and are managed by fastn, including OAuth refresh.

{% hint style="warning" %}
Deleting a secret takes effect immediately. Any workflow calling `fastn.secrets.get` for that name starts failing on its next run.
{% endhint %}

For non-sensitive per-environment values, use [Configs](/manage/configs) instead.


# Configs

Values your workflow code reads per environment.

**Settings → Configs**

With nothing configured yet, the page reads **No configs yet** above the line that explains the feature:

<figure><img src="/files/ixNNUyKhA4gTAtcQDrm1" alt="The Configs page holding only a No configs yet empty state, explaining that a config holds anything changing between test and live, with Add your first config centred and Add config top-right"><figcaption><p>The Configs page before any config exists. Read a config from workflow code with <code>fastn.envConfig.get("key")</code>.</p></figcaption></figure>

> A config holds anything that changes between test and live, like an endpoint or a feature flag. Read one with fastn.envConfig.get.

```javascript
const endpoint  = await fastn.envConfig.get("PARTNER_API_BASE");
const batchSize = await fastn.envConfig.get("BATCH_SIZE");
```

### Creating one

**Add config** opens the **Add Config** dialog, which takes two things.

**Key \*** is the string your code passes — `fastn.envConfig.get("PARTNER_API_BASE")` reads the config whose key is `PARTNER_API_BASE`.

**Values per environment** is one editor per environment, labelled with the display name and the slug: **Test (test)**, **Live (live)**, and a row for each named environment you have added. A value may be raw text or valid JSON.

{% hint style="info" %}
**Leaving an environment's editor blank skips that environment on save** — it does not write an empty value. That is usually what you want when you are only setting up test, but it means a config can silently have no value in live.
{% endhint %}

The same key resolves to a different value depending on which environment the run is in, so your code never branches on environment.

### Configs versus secrets

A config's value is entered and edited in the dashboard rather than written once and hidden, which is the practical difference from a [secret](/manage/secrets): you can see what an environment is set to without running anything. Treat that as the working assumption — anything whose exposure would be an incident belongs in a secret regardless.

### Choosing between a config and a secret

| Use a **config** for                        | Use a **secret** for                   |
| ------------------------------------------- | -------------------------------------- |
| Base URLs and endpoints                     | API tokens and passwords               |
| Feature flags                               | Signing and encryption keys            |
| Batch sizes, page sizes, thresholds         | Database connection strings            |
| Anything you would happily show a colleague | Anything whose exposure is an incident |

{% hint style="warning" %}
When in doubt, use a secret. A config that turns out to be sensitive is already visible to everyone who can read settings.
{% endhint %}

### A common pattern

Point test at a sandbox and live at production, without a line of conditional code:

| Config             | test                              | live                          |
| ------------------ | --------------------------------- | ----------------------------- |
| `PARTNER_API_BASE` | `https://sandbox.partner.example` | `https://api.partner.example` |
| `BATCH_SIZE`       | `10`                              | `500`                         |
| `DRY_RUN`          | `true`                            | `false`                       |

Which environment a run uses is decided by the `x-fastn-env` header or the trigger route — see [Environments](/manage/environments).


# Environments and GitHub

Deployment stages, and the GitHub review gate in front of them.

**Settings → Environments**

<figure><img src="/files/lIF9RP0STjeypXikSRXE" alt="The Environments page: a GitHub card reading No repository connected beside a Connect GitHub button, and a single table row — Live, slug live, Default, Requires review off, Protected"><figcaption><p>What a workspace looks like before anyone adds a stage: one built-in Live target and no review gate in front of it.</p></figcaption></figure>

> Test and Live are built in. Add a named environment here when you want another stage, such as staging or review.

### Environments

| Column              | Notes                                                                                                             |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Name**            | Display name.                                                                                                     |
| **Slug**            | Used in the `x-fastn-env` header and in trigger routes.                                                           |
| **Type**            | Default, or a named environment you created.                                                                      |
| **Requires review** | A toggle per environment, off by default. Switched on, promoting to it opens a pull request instead of deploying. |

Live carries a **Protected** badge. Read that as fastn marking it as the one you should not casually restructure; whether the badge also hard-blocks deletion is not something this page can confirm.

**New environment** opens the **Create Environment** dialog, which adds a stage and asks for two things:

| Field       | Notes                                                                                                                         |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Name \*** | Display name, for example `Staging`.                                                                                          |
| **Slug \*** | What goes in `x-fastn-env` and in trigger routes. The form notes that `test` and `live` are built in, so those two are taken. |

A named environment runs whatever version is deployed to it. If nothing has been deployed there, there is no code for a trigger pointed at it to run — deploy before you point traffic at a new stage.

### The special value `test`

In trigger routes and in the `x-fastn-env` header, `test` is not a deployed environment — it means *the workflow's latest published version*. Any other slug means *the version deployed to that environment*. That is why the table is a list of deployment targets, with `test` handled by the platform rather than sitting among them.

### GitHub

A **GitHub** panel sits above the table, explaining what connecting a repository buys you:

> Connect a repository and promoting to a reviewed environment opens a pull request instead of deploying straight away.

Until you connect one, its status reads **No repository connected** and the only control is **Connect GitHub**. That is what a new workspace sees, and it is worth knowing that the review gate does nothing on its own — it needs both a repository here and **Requires review** switched on for the environment you are promoting into.

Once a repository is connected the panel shows it, along with controls for mirroring workflows to the repo and for disconnecting. Those controls were not captured on a connected workspace, so check what disconnecting does to anything already deployed before you use it rather than assuming it is inert.

### A workable setup

1. Connect the repository.
2. Add a `staging` environment.
3. Tick **Requires review** on it.
4. Developers publish; the promotion opens a pull request; the reviewed and merged version deploys to staging.
5. Promote from staging to live once it has run against real traffic.

{% hint style="success" %}
The repository plus **Requires review** is the single change that turns fastn from a place where anyone can push to production into one with a real approval trail. If you have more than a couple of people building, do it.
{% endhint %}

Publishing and deploying are both recorded in the [audit log](/manage/audit-log).


# Database

Which Postgres your workflows read and write through.

**Settings → Database**

<figure><img src="/files/9u1mwUPM9SJvNHFUdZhk" alt="Database settings: a Writing to your database banner over two cards, Shared (fastn runs it) unselected and Your own (You run it) selected, each listing Backups, Capacity, Uptime and Network setup"><figcaption><p>The choice is exclusive and it is a redirect, not a migration — rows already written stay where they are.</p></figcaption></figure>

Workflows can persist data with `fastn.db`. This page decides where that data lands.

> Which Postgres your workflows read and write through.

Every `fastn.db` call from this organisation goes to whichever option is selected here, and the page shows the active schema name in the form `ws_<32 hex characters>`.

### What a Developer sees

The page is read-only below Owner and Admin. A Developer gets the mode and the identifiers and nothing else:

| Row                | Value                                      |
| ------------------ | ------------------------------------------ |
| Mode               | **Shared**                                 |
| **DATABASE**       | `fastn-managed`                            |
| **MANAGED SCHEMA** | `ws_<hash>` — your workspace's schema name |

Username, password and CA chain are not rendered for this role. That is deliberate, and it is enough: `fastn.db` connects for you, so a workflow never needs the credential. There is no table browser and no SQL console on this page — inspect data from a workflow.

### Two options

|                   | **Shared** — fastn runs it | **Your own** — you run it |
| ----------------- | -------------------------- | ------------------------- |
| **Backups**       | fastn                      | You                       |
| **Capacity**      | fastn                      | You                       |
| **Uptime**        | fastn                      | You                       |
| **Network setup** | None                       | Allow our runners         |

**Shared** — fastn runs it, and handles backups, patching, upgrades and capacity. Your workspace sits in its own schema, isolated from everyone else.

**Your own** — you run it: workflows connect to a Postgres you operate. You keep full control of the data and full responsibility for it, and you will need to allow fastn's runners through your network.

### Reasons to bring your own

Data residency requirements, a compliance regime that will not accept a processor's database, or an existing warehouse you want workflow data to land in directly.

If none of those apply, Shared is less work and one fewer thing to page you at 3am.

{% hint style="danger" %}
Switching changes where **new** reads and writes go. Rows are never copied between the two. If a workflow depends on data written before the switch, it will not find it. Migrate deliberately, or not at all.
{% endhint %}

### What the isolation actually is

**The unit of isolation is the workspace, not the customer.** Each workspace gets its own Postgres schema — the `ws_<hash>` above — isolated from every other workspace, which is what the page means by *your workspace sits in its own schema, isolated from everyone else*.

That is a boundary between you and other fastn customers. It is **not** a boundary between *your* customers: rows written on behalf of one of your customers and rows written on behalf of another land in the same schema. If you need per-customer separation inside it, build it — a customer column on every table, and a predicate on every query.

### Using it from a workflow

```javascript
await fastn.db.query(`
  CREATE TABLE IF NOT EXISTS sync_log (
    id SERIAL PRIMARY KEY,
    customer_id TEXT,
    source_id TEXT,
    synced_at TIMESTAMP DEFAULT NOW()
  )
`);

await fastn.db.query(
  `INSERT INTO sync_log (customer_id, source_id) VALUES ($1, $2)`,
  [customerId, record.id]
);
```

{% hint style="info" %}
The exact `fastn.db` call signature — whether it is `query(sql, params)` and whether parameters are `$1`-style — is worth confirming against a workflow's own **Docs** tab, which is generated from the runtime you are actually calling. See [Workflow runtime API](/reference/workflow-runtime).
{% endhint %}


# SaaS Connectors

Registering your own SaaS API as a connector, and the scopes tenants connect under.

**Settings → SaaS Connectors**

> Register your own SaaS API's connectors and define the connection scopes tenants are provisioned under.

Every other connector in fastn represents someone else's system — Shopify, HubSpot, Jira. This screen is for the opposite case: **your own product's API**, registered so that your tenants' connections can be provisioned against it.

<figure><img src="/files/iyNNmcGbf5fAuEf4YK6b" alt="The SaaS Connectors settings page in its empty state, reading No SaaS connectors registered yet above an Add your first SaaS connector button"><figcaption><p>Before anything is registered. The empty state repeats the whole concept, scopes included.</p></figcaption></figure>

With nothing registered the page reads:

> No SaaS connectors registered yet. Register your own SaaS API's connector and define connection scopes (e.g. inventory, product) that your tenants' connections are provisioned under.

### Registering one

**Add your first SaaS connector** (or **Add**, once the list is populated) opens the **Add SaaS Connector** panel, which takes two things.

| Field                 | Notes                                                                                                                                                                                                                                                                                                                   |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Connector \***      | A picker over the whole connector catalogue — *Select your SaaS API's connector…*. Its helper text is the important part: *"The connector for your own SaaS API — the one your tenants' connections are provisioned against."* This is not the system you are integrating **with**; it is the one representing **you**. |
| **Connection scopes** | A free-text field with an **Add** button beside it, seeded with the example `inventory`. Add as many as you need.                                                                                                                                                                                                       |

The panel closes with **Cancel** and **Add SaaS Connector**.

### What a scope is for

The helper text under the scopes field states the rule directly:

> Each tenant holds at most one connection per scope (e.g. inventory, product). A widget tags its SaaS connector with a scope, and activation pins the tenant's matching connection.

Three consequences worth being deliberate about:

* **One connection per tenant per scope.** A scope is the uniqueness key. A tenant cannot hold two `inventory` connections to your API, so a scope is how you stop a customer from accidentally wiring the same thing twice.
* **Widgets tag against a scope.** The [widget](/embed/widget-builder) names the scope it wants, which is what lets one widget resolve to the right connection for whichever tenant is looking at it.
* **Activation pins the match.** When a tenant activates, the connection matching that scope is the one bound — so the scopes you choose here decide how granular per-tenant provisioning can be.

Pick scopes that name a *capability boundary* in your own API — `inventory`, `product`, `billing` — rather than one scope per customer. Customers are already the tenant axis; scopes are the second axis crossing it.

{% hint style="info" %}
This screen sits in Settings alongside [Environments](/manage/environments) and [Configs](/manage/configs) rather than under Build, because registering your own API is an organisation-level act rather than something you do per integration.
{% endhint %}

{% hint style="warning" %}
How a scope behaves once connections exist against it — whether it can be renamed or removed with live tenant connections attached — is not documented here. Register a scope you intend to keep, and confirm the behaviour in your own workspace before restructuring one that is in use.
{% endhint %}


# Billing and limits

Your plan, what you have used, and where the ceilings are.

**Settings → Billing**

<figure><img src="/files/hDuQea5Ps5UGJcfepmZc" alt="The Billing page with a Free, $0 per month plan card, a Credits this period tile reading 0 of 50 used and resetting in 27 days, and a Limits list where API keys per customer reads 4 / 2, 100%"><figcaption><p>Rows marked not measured have a cap but no usage reading behind them.</p></figcaption></figure>

### Plan and credits

The plan card shows what you are on and what it costs — **Free**, at **$0 per month**, until you upgrade. **See plans** is the upgrade control, and the only one on the page.

**Credits this period** tracks AI usage against the allowance. It is the same number as the AI credits readout in the top bar, and the popover behind that readout is where the detail lives: tabs for **Your usage** and **Org total**, a **By agent** breakdown, and a reset at the start of the calendar month, UTC.

### Limits

Each quota is listed with usage against it. **Customize customer limits** overrides limits for a single customer.

| Limit               | Scope               |
| ------------------- | ------------------- |
| API keys            | per customer        |
| Events              | per day, per minute |
| API calls           | per day, per minute |
| Active integrations | total               |
| Connectors allowed  | total               |
| Workflows           | per customer        |
| Steps               | per flow            |

The list continues past the visible area with further rows — concurrency, AI sessions and tokens, retention, storage, connected accounts, users, webhook endpoints, payload size, executions and AI credits are all reported to appear further down. Scroll the page and read the rows themselves rather than relying on that list being complete or the scopes being as described.

{% hint style="info" %}
Going over a limit **stops new work rather than charging you, and nothing already running is interrupted.** No surprise invoices, but also no silent overage — a sync that stops because you hit a ceiling looks like a broken sync until you check this page.
{% endhint %}

### Below the fold

Sections further down this page — **Customer tiers** and **Create tier**, **Usage by customer**, **Payment** and **Invoices** — have not been captured, so this page does not describe how they behave. Read them in the product before planning around them.

One thing about tiers is corroborated from elsewhere: the [Roles](/manage/roles) screen states that a custom role can be assigned to an embed tier under Billing, which is how you scope what an embedded end user may do.


# Audit log

Every action taken in this organisation.

**Settings → Audit log**

<figure><img src="/files/AuSCtaKoUqAjZK7UqF1V" alt="The Audit log counting 4304255 events, over rows of connection.update attributed to apikey:8 and workflow.execute and auth.login attributed to people whose names are obscured — each Success, scoped to a customer id or Organisation-wide"><figcaption><p>Filters for people, actions, types and a date range sit above the table, with Export top-right.</p></figcaption></figure>

A complete, filterable record of everything anyone — or anything — did. The header carries the total, which runs high: a working organisation accumulates millions of events.

### The table

| Column       | Notes                                                                           |
| ------------ | ------------------------------------------------------------------------------- |
| **Who**      | A person, or an API key in the form `apikey:8`.                                 |
| **Action**   | The action name — `auth.login`, `workflow.execute`, `connection.update`.        |
| **Resource** | The object acted on, with its type underneath.                                  |
| **Customer** | The customer scope, or **Organisation-wide**.                                   |
| **Result**   | **Success** on every observed row. How a failure renders has not been captured. |
| **When**     | Timestamp.                                                                      |

### Filters

Search by person, action or resource; then narrow by **people**, **actions**, **types**, and a date range. **Export** downloads the set currently filtered — check the file it produces for its format before wiring anything to it.

### Action families

Actions are namespaced, which is what makes filtering practical: `auth.login`, `workflow.execute` and `workflow.execute.completed` are all confirmed, and the pattern is `<resource>.<verb>`, sometimes with a terminal state appended.

Beyond those three, the surest way to see what your organisation actually emits is the **All actions** dropdown on this page — it lists the real vocabulary. Expect namespaces along these lines, but confirm there before you build a filter or an alert on one:

| Prefix                         | Likely covers                                                                               |
| ------------------------------ | ------------------------------------------------------------------------------------------- |
| `auth.*`                       | Logins. `auth.login` confirmed.                                                             |
| `workflow.*`                   | Workflow lifecycle and runs. `workflow.execute` and `workflow.execute.completed` confirmed. |
| `api_key.*`                    | Key creation, rotation and revocation.                                                      |
| `connection.*`, `credential.*` | Connection lifecycle and token refresh. `connection.update` confirmed.                      |
| `connector.*`                  | Connector creation, publishing and deletion.                                                |
| `secret.*`, `config.*`         | Secret and config changes — who and when, not values.                                       |
| `user.*`, `membership.*`       | Access changes.                                                                             |
| `environment.*`                | Environment changes.                                                                        |

### Who can read it

Restricted to account owners and admins. This is enforced by role at the API layer — `/api/v1/audit-log` — and cannot be granted or revoked per user. See [People and roles](/manage/roles).

### What to look for

| Question                                | Where to start                                                          |
| --------------------------------------- | ----------------------------------------------------------------------- |
| Who changed this workflow?              | Filter to the workflow as the resource, then pick its update action.    |
| Why did a connection start failing?     | Filter to that customer, then to the connection and credential actions. |
| Who created that API key?               | Filter to the key-creation action in **All actions**.                   |
| What did this key do?                   | Filter by the key — it appears under **All people** as `apikey:<n>`.    |
| Did anyone touch production last night? | The deploy action plus the date range.                                  |

{% hint style="info" %}
Filter before you scroll, and export when you need to analyse. At this volume, scrolling is not a search strategy.
{% endhint %}


# Trash

Deleted connectors, actions and workflows, kept until you remove them.

**Settings → Trash**

<figure><img src="/files/ik3Gg1TcqblKT1lAFwlI" alt="The Trash page on its Connectors tab, beside Actions and Workflows, showing a Nothing deleted empty state and the line that deleted connectors land here, so a mistake is a restore"><figcaption><p>The paragraph underneath warns that widgets are deleted immediately and cannot be restored here.</p></figcaption></figure>

> Deleted connectors, actions and workflows, kept until you remove them.

Three tabs: **Connectors** (the default), **Actions** and **Workflows**. The Workflows tab lists:

| Column                 | Notes                              |
| ---------------------- | ---------------------------------- |
| **Name**               | The workflow's display name.       |
| **Slug**               | Restored unchanged — see below.    |
| **Deleted**            | When it was moved here.            |
| **In trash**           | How long it has been sitting here. |
| **Restore**            | Puts it back.                      |
| **Delete permanently** | Removes it for good.               |

{% hint style="warning" %}
**The Actions tab currently does not load.** It sits on *Loading deleted actions…* and never resolves. Deleted connector actions are still tracked — this is a defect in the tab, not evidence that nothing is there — but you cannot restore one from this screen while it persists.
{% endhint %}

### What restore gives you back

Anything here is restored with its slug and history intact. That matters — a workflow that referenced a connector by slug keeps working after a restore, which would not be true if you rebuilt it from scratch.

### Nothing expires

Nothing is removed automatically. Items stay until you use **Delete permanently** to clear one for good.

{% hint style="danger" %}
**Delete permanently** cannot be undone. There is no second trash.
{% endhint %}

### What does not come here

> Other resources — widgets and their integrations among them — are deleted immediately and cannot be restored from this page.

So the recoverable set is exactly connectors, connector actions and workflows. Anything else — a widget and the integrations configured on it, explicitly — goes when you delete it.

### In the audit log

Soft deletes, restores and permanent deletes are recorded separately from one another, which is what lets you reconstruct what happened to something after the fact. Filter by the resource on the [audit log](/manage/audit-log), and use its **All actions** dropdown for the exact action names your organisation emits.


# Your profile

Your account and how you sign in. Organisation settings live separately.

The profile row at the bottom of the left rail, or `/profile`

<figure><img src="/files/esm9ZOky2wx02yUfVE1R" alt="Your profile: an account card badged Owner with the name and address obscured, an About you section holding First name, Last name, an empty Job title placeholdered Head of Platform and Timezone Asia/Karachi, then a Sign in email section whose address is obscured"><figcaption><p>The sign-in address is read-only here because an identity provider manages it.</p></figcaption></figure>

This page is about you, not the organisation. The **Organisation settings** link crosses over to [General](/manage/general).

### About you

Shown to your team on invitations, comments and audit entries.

| Field          | Notes                                                           |
| -------------- | --------------------------------------------------------------- |
| **First name** | Required.                                                       |
| **Last name**  | Required.                                                       |
| **Job title**  | Optional.                                                       |
| **Timezone**   | The full IANA list. Every time on screen is shown in this zone. |

{% hint style="info" %}
Your profile timezone changes how timestamps are *displayed* to you. It does not change when a [schedule trigger](/build/triggers) fires — a schedule keeps the timezone it was saved with.
{% endhint %}

### Sign in email

Your sign-in address, and where security notices go. It shows a **Verified** badge once confirmed.

Where sign-in is managed by an identity provider, the address is read-only and the page says so — an admin changes it, not you.

### Security

Three rows, each showing its current state — **Not set up** until you configure it, with 2FA also flagged **Recommended**.

| Control                       | Notes                                                                                                                        |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Two factor authentication** | **Recommended**, and **Not set up** by default. A code from an authenticator app, in addition to your magic link or passkey. |
| **Passkeys**                  | **Not set up** by default. Sign in with Face ID, a fingerprint or a hardware key.                                            |
| **Password**                  | Sign-in is passwordless — magic links and passkeys. There is no password to set or change.                                   |

{% hint style="info" %}
There is no password on a fastn account, so a second factor is not a backstop behind one — it is the second thing in front of an account that otherwise rests on access to an inbox or a device. If your account is an Owner or Admin, it can change anything in the organisation. Set one up.
{% endhint %}

### Where you are signed in

Every active session, with its **IP** address, newest marked **Most recent**.

| Button         | Effect                                                                            |
| -------------- | --------------------------------------------------------------------------------- |
| **Sign out**   | Ends that one session. It sits on each row.                                       |
| **Everywhere** | Ends all of them. Use if you have lost a device or think someone else has access. |

Sign out anything you do not recognise, then check the [audit log](/manage/audit-log) for `auth.login` entries from the same address.

### Switching organisation

The account card at the bottom of the left rail switches between organisations you belong to. Everything else in the dashboard — connectors, workflows, customers, settings — is scoped to whichever one is selected.


# Workflow runtime API

What a workflow can reach at runtime — ctx, multi-tenant headers, connectors, unified APIs, database, state, secrets and configs.

Every workflow is an async function that receives a context object, does its work, and returns a result.

```javascript
export default async function (ctx) {
  // ...
  return { ok: true };
}
```

***

## ctx.input

The incoming payload. What it contains depends on what started the run.

| Trigger          | `ctx.input` holds                                                                                                                             |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Webhook**      | The HTTP request body.                                                                                                                        |
| **Schedule**     | Whatever JSON you put in the route's **Payload** field. A schedule has no payload of its own, so this is how a scheduled run gets parameters. |
| **App event**    | The event payload from the connector.                                                                                                         |
| **Manual / API** | The `input` object from the request body.                                                                                                     |

The shape is declared on the workflow's **Contract** tab, which is also what fills the Test tab and the API examples.

```javascript
const { saleId, syncMode, connection_id } = ctx.input;
```

## ctx.headers

HTTP headers from the incoming request, for webhook-triggered workflows.

```javascript
const contentType = ctx.headers['content-type'];
```

`ctx.headers` is also how a run knows which of your customers it is acting for — see [Multi-tenant headers](#multi-tenant-headers) below.

## ctx.connectors

The connectors bound to this workflow, available on the context object alongside `ctx.input` and `ctx.headers`. What is bound is what the workflow's **Connectors** tab lists, so that tab is the authoritative view of what a run can reach.

***

## Multi-tenant headers

fastn is multi-tenant, and these five headers on the incoming request are how a call says which customer it is for and what it may use. Getting them right is the difference between a workflow that serves one customer and one that serves all of them.

| Header                        | Carries                                                     |
| ----------------------------- | ----------------------------------------------------------- |
| `x-end-org-id`                | The customer the run acts for.                              |
| `x-end-org-ref`               | Your own reference for that customer.                       |
| `x-installation-id`           | Which installation of the integration this run belongs to.  |
| `x-fastn-connections`         | The connections the run may use.                            |
| `x-fastn-installation-config` | The configuration values that installation was set up with. |

They are documented on every workflow's **Docs** tab, generated from the runtime you are actually calling — read exact shapes and value formats there before wiring them into a caller.

***

## Calling a connector

Calls actions on connected systems, by connector slug.

```javascript
const sale    = await fastn.connector.cin7core.getSale({ saleId });
const created = await fastn.connector.trackstar.createOrder({ ... });
```

{% hint style="warning" %}
**The product is inconsistent about the name here.** A workflow's **Docs** tab documents `fastn.connector` (singular), while the **Connectors** tab describes auto-extracting bound connectors from `fastn.connectors.X.Y(…)` calls (plural) when you save. Check your own workspace's Docs tab and confirm that saving picks up your calls on the Connectors tab — if the extraction misses them, you are on the wrong spelling.
{% endhint %}

The slug is the one on the connector's Overview tab. Available actions and their versions are pinned on the workflow's **Connectors** tab, and each connector there is marked *per customer* or workspace — which decides whose credential the call uses.

***

## fastn.unified

Calls a [unified API](/build/unified-apis) — one canonical entity, served by whichever provider the running customer has connected — rather than naming a specific connector.

This is the runtime counterpart to the `GET|POST /api/v1/unified/{category}/{entity}` endpoints on the Unified APIs page. Use it wherever you would otherwise write a branch per CRM: the routing to hubspot, salesforce or zohoCrm is the platform's problem rather than your code's. The exact call shape is on the workflow's **Docs** tab.

***

## fastn.db

SQL against your workspace's Postgres schema.

**The isolation unit is the workspace, not the customer.** Each workspace gets its own schema — named `ws_<hash>` — isolated from every other workspace. Rows written for one of *your* customers and rows written for another sit in that same schema together. Nothing scopes a query to a customer for you: if you need that separation, put a customer column on the table and a predicate on every query.

```javascript
await fastn.db.query(
  `INSERT INTO sync_log (customer_id, source_id) VALUES ($1, $2)`,
  [customerId, record.id]
);

const rows = await fastn.db.query(
  `SELECT * FROM sync_log WHERE customer_id = $1 AND source_id = $2`,
  [customerId, record.id]
);
```

Which Postgres this reaches is set under [Settings → Database](/manage/database).

{% hint style="info" %}
The exact signature above — `query(sql, params)` with `$1`-style placeholders — should be confirmed against your workflow's **Docs** tab, which is generated from the runtime you are calling.
{% endhint %}

{% hint style="warning" %}
Always parameterise. String-interpolating a value from `ctx.input` into SQL is an injection waiting to happen.
{% endhint %}

***

## fastn.state

Durable key-value storage across runs, in one of two scopes.

| Scope          | Lifetime               | Use for                                   |
| -------------- | ---------------------- | ----------------------------------------- |
| **ORG**        | Outlives a single run. | Deduplication, synced-record IDs, caches. |
| **INVOCATION** | Bounded to one run.    | Temporary state inside a long run.        |

{% hint style="warning" %}
**Confirm what `ORG` actually spans before you rely on it.** The two scope names are what the runtime documents; whether `ORG` means org-wide across every workflow, or is partitioned per workflow, is not settled here — and the difference matters. Org-wide, a key like `deal:123` collides between two workflows that both process deals. Check your workspace's **Docs** tab, or namespace your keys by workflow so it does not matter either way.
{% endhint %}

```javascript
const key  = `myworkflow:deal:${dealId}`;
const seen = await fastn.state.get(key);
if (seen) {
  return { success: true, skipped: true, reason: "already_processed" };
}
await fastn.state.set(key, { processedAt: new Date().toISOString() });
```

That is the standard idempotency guard. Pair it with a [deduplication key](/build/triggers) on the trigger and a retried delivery is much less likely to double-write — verify the guard actually holds under a replay in your own workspace before treating it as a guarantee.

***

## fastn.secrets

Reads encrypted values from [Settings → Secrets](/manage/secrets).

```javascript
const token = await fastn.secrets.get("SHOPIFY_API_TOKEN");
```

The string you pass is the secret's **Name** exactly, in UPPER\_SNAKE\_CASE. A secret of type JSON comes back already parsed.

## fastn.envConfig

Reads per-environment values from [Settings → Configs](/manage/configs).

```javascript
const base = await fastn.envConfig.get("PARTNER_API_BASE");
```

***

## fastn.diff.compare

Produces a [sync report](/operate/sync-reports) — a record-by-record account of what a run changed. A workflow that calls it gets a report; one that does not, does not.

***

## Returning

Whatever you return becomes the workflow's output, and must match the output contract. The tier decides who waits for it:

| Tier         | The caller gets                         | Ceiling    |
| ------------ | --------------------------------------- | ---------- |
| **Instant**  | The return value inline, synchronously. | 30 seconds |
| **Standard** | `202` and an execution id.              | 15 minutes |
| **Long**     | `202` and an execution id.              | 36 hours   |

**Instant's ceiling is 30 seconds.** It is short on purpose — it is the tier for something a caller is blocked on. Anything that reaches out to two or three systems in sequence belongs on Standard, and finding out by timing out in production is the expensive way to learn it.

## Errors

Throwing marks the execution **Failed** and, where a retry policy is enabled, triggers a retry. Code errors, data errors and out-of-memory never retry — only transient failures do.


# HTTP API

Calling workflows and unified endpoints over HTTP.

<figure><img src="/files/wXh8V6c43miEokkiUdx3" alt="A workflow&#x27;s API tab showing a POST endpoint ending /workflows/wf_b5880b29eb25/execute, a copyable curl carrying Authorisation, X-fastn-Test-Mode and x-fastn-env headers, and a Request body block"><figcaption><p>Every workflow's API tab shows its own endpoint and a copyable curl.</p></figcaption></figure>

### Base URL

The base URL is the deployment you are on — for example `https://app.fastn.dev` on the production platform. Your workflow's **API** tab always shows the correct one for your workspace; the examples below use `YOUR_FASTN_HOST`.

***

## Authentication

Every request carries an API key as a bearer token.

```
Authorization: Bearer fsk_live_<your-key>
```

Keys are created under [Settings → API keys](/manage/api-keys) and come in two modes.

| Header              | Live key             | Test key                                              |
| ------------------- | -------------------- | ----------------------------------------------------- |
| `Authorization`     | `Bearer fsk_live_…`  | `Bearer fsk_test_…`                                   |
| `X-fastn-Test-Mode` | Not needed           | Must be `true` — **a test key is refused without it** |
| `x-fastn-env`       | Any environment slug | Any environment slug                                  |

The rule that is enforced is the test-key one: a test key without `X-fastn-Test-Mode: true` is rejected. Do not read that backwards into an assumption about what a live key may or may not send.

`x-fastn-env` picks which code runs:

* `test` — the workflow's latest published version.
* any other slug — the version deployed to that environment.

{% hint style="danger" %}
A test key is not a sandbox. It reaches the same live connections as a live key and causes the same real writes.
{% endhint %}

***

## Execute a workflow

```
POST /api/v1/workflows/{workflowId}/execute
```

The workflow id is shown on the workflow's API tab, in the form `wf_b5880b29eb25`.

```bash
curl -X POST https://YOUR_FASTN_HOST/api/v1/workflows/WORKFLOW_ID/execute \
  -H "Authorization: Bearer fsk_test_<your-key>" \
  -H "X-fastn-Test-Mode: true" \
  -H "x-fastn-env: test" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "saleId": "S-1024",
      "isRetry": false,
      "syncMode": "incremental"
    }
  }'
```

### Request body

```json
{
  "input": {
    "key": "value"
  }
}
```

The `input` object is what arrives as `ctx.input`. Its shape comes from the workflow's input contract.

{% hint style="info" %}
Every workflow's **API** tab generates a ready-to-run curl filled in with that workflow's own host, id and contract fields. When the two differ, copy from there — it is generated from the deployment you are actually calling.
{% endhint %}

### Response — Instant tier

The workflow's return value comes back inline, synchronously, within the tier's 30-second ceiling. Its shape is the workflow's own **output contract** — read it on the Contract tab rather than from an example here, because it is different for every workflow.

### Response — Standard and Long tiers

`202 Accepted` with an execution id, in the form `exec_…`. The run continues in the background; the id is what identifies it afterwards in [Executions](/operate/executions), where each row expands to its input, output and per-step timings.

{% hint style="info" %}
There is no documented endpoint for polling an execution id. If your caller needs to know the outcome of a Standard or Long run, have the workflow call you back at the end rather than assuming you can poll for it.
{% endhint %}

***

## Scoping a call to a customer

fastn is multi-tenant: the same workflow serves all of your customers, and headers on the request are what say which one a call is for.

| Header                        | Carries                                                     |
| ----------------------------- | ----------------------------------------------------------- |
| `x-end-org-id`                | The customer this call acts for.                            |
| `x-end-org-ref`               | Your own reference for that customer.                       |
| `x-installation-id`           | Which installation of the integration the call belongs to.  |
| `x-fastn-connections`         | The connections the run may use.                            |
| `x-fastn-installation-config` | The configuration values that installation was set up with. |

Inside the workflow these arrive on `ctx.headers` — see [Workflow runtime API](/reference/workflow-runtime). Exact value formats are on each workflow's own **Docs** tab, generated from the deployment you are calling; check there before you hard-code one.

***

## Unified endpoints

Where a [unified API](/build/unified-apis) covers an entity, call it directly rather than a specific connector.

```http
GET  /api/v1/unified/crm/account?page_size=50
GET  /api/v1/unified/crm/account/{recordId}
POST /api/v1/unified/crm/account
POST /api/v1/unified/crm/note
```

fastn routes to whichever provider the customer authorised. Each endpoint on the Unified APIs page has a **Copy curl** button with the correct headers filled in.

***

## Webhook endpoints

A [webhook trigger](/build/triggers) gives you a public URL. Whether callers must authenticate is set on the trigger:

| Authentication setting           | Caller must send                 |
| -------------------------------- | -------------------------------- |
| **API Key (x-fastn-access-key)** | `x-fastn-access-key: <your-key>` |
| **None (public)**                | Nothing                          |

***

## Embed tokens

Tokens that scope the widget to one customer. Mint them from your backend with your API key; never expose the API key to a browser.

### Mint a token

```
POST /api/v1/embed/token
```

| Header          | Value              |
| --------------- | ------------------ |
| `Authorization` | `Bearer <API key>` |
| `x-org-id`      | `<orgId>`          |

```json
{
  "token": "emb_…",
  "endOrgId": "…",
  "role": "end_user",
  "expiresIn": 28800
}
```

`expiresIn` is **28800 seconds — eight hours**. `endOrgId` is the customer the token is scoped to, and the returned role is always `end_user`.

Where the API key is pinned to specific customers, send `{"endOrgId": "…"}` in the request body instead of the `x-org-id` header.

### Refresh

```
POST /api/v1/embed/token/refresh
```

{% hint style="warning" %}
**Refresh is capped at seven days per session.** At the cap the widget posts `fastn:session-expired` to the parent window and stops — refreshing again does not extend it. Listen for that message and start a new session by minting a fresh token. A host app that assumes refresh is indefinite will strand long-lived sessions.
{% endhint %}

### Using the token

The iframe endpoint takes it on the query string:

```
https://YOUR_FASTN_HOST/api/v1/embed/iframe?token=emb_…
```

That URL carries a live credential. Treat it like one — build it server-side per session, and do not log or share it. Full setup in [Embedding the widget](/embed/embedding).

***

## Errors

| Response                 | Means                                                                                                                                                                                                                       |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WORKFLOW_NOT_PUBLISHED` | The workflow has never had a snapshot published, so there is no version to run. **Every call returns this until you publish one** — it is the most common cause of "the API did nothing". Publish from the workflow editor. |
| `401` / `403`            | The key is wrong, revoked, expired, outside its IP allowlist, or lacks the permission for what you called. Check the key on [Settings → API keys](/manage/api-keys).                                                        |
| Test key rejected        | The key is a test key and `X-fastn-Test-Mode: true` was not sent.                                                                                                                                                           |

A workflow that runs and throws is a **Failed** execution rather than a transport error — look for it in [Executions](/operate/executions), not in the HTTP response.

***

## Rate limits

Per-day and per-minute ceilings on API calls and events are listed, with current usage, on [Settings → Billing](/manage/billing), which also carries per-customer limits. Going over stops new work rather than charging you, and nothing already running is interrupted — so a caller that suddenly gets nowhere is worth checking against that page before you debug the workflow.


# fastn for AI agents

A single dense page for LLMs and coding agents: the model, the runtime surface, the endpoints, and the things that are easy to get wrong.

This page exists for AI agents and other machine readers. It is deliberately terse and factual: no narrative, no screenshots, every exact string spelled out. Human readers are better served by [Core concepts](/fastn/readme/concepts).

If you are an agent writing fastn workflows or calling the fastn API, read this page first, then confirm anything marked **Verify** against the live surfaces named at the bottom.

***

## The model in six lines

1. An **organisation** is you, the SaaS company. It holds everything below.
2. A **connector** defines an external system — its actions, auth methods and versions.
3. A **connection** is *one of your customers'* authorised link to one connector, holding the encrypted credential.
4. A **workflow** is JavaScript that runs on a trigger. It is code, not a drag-and-drop graph.
5. A **trigger** starts a workflow: webhook, schedule, or app event.
6. A **widget** is the integrations panel your customers see, embedded in your product.

fastn is multi-tenant throughout. One workflow serves every customer; headers on the request decide which customer a run acts for.

***

## Workflow anatomy

A workflow is an async function receiving a context object.

```javascript
export default async function (ctx) {
  const { recordId } = ctx.input;
  const record = await fastn.connector.hubspot.getContact({ recordId });
  return { ok: true, id: record.id };
}
```

* The file is `<slug>.js`. The export is default and async.
* The **Diagram** tab is generated *from* this code and is read-only. There is no visual editor to keep in sync.
* The editor autosaves. There is no Save button.
* Whatever you return must match the workflow's declared output contract.
* Throwing marks the execution **Failed**, and retries only where a retry policy is enabled.

{% hint style="warning" %}
**Do not generate TypeScript.** Workflows are JavaScript.
{% endhint %}

{% hint style="danger" %}
**Code editing is switched off in almost every workspace** — enabled only for the parent organisation. In every other workspace the editor has no code column, and workflows are written and updated by the fastn agent from a natural-language description. Handing a user a code block to paste into the editor will not work there. Describe the change to the agent instead, or tell the user to ask fastn to enable code editing. The runtime surface documented below is identical either way, so this page is still the right reference for *what* the generated code can do.
{% endhint %}

### `ctx`

| Property         | Contains                                                                                                                                                                                 |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.input`      | The incoming payload. Webhook → request body. Schedule → the route's **Payload** JSON. App event → the connector's event payload. Manual/API → the `input` object from the request body. |
| `ctx.headers`    | HTTP headers from the incoming request, including the multi-tenant headers below.                                                                                                        |
| `ctx.connectors` | The connectors bound to this workflow — the same set listed on its **Connectors** tab.                                                                                                   |

### Runtime surface

| Call                                                   | Purpose                                                                                                             |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `fastn.connector.<slug>.<action>(args)`                | Call an action on a connected system. **Verify** — see the naming caveat below.                                     |
| `fastn.unified.…`                                      | Call a unified entity and let fastn route to whichever provider the customer connected.                             |
| `fastn.db.query(sql, params)`                          | SQL against your workspace's Postgres schema (`ws_<hash>`). **Verify** signature.                                   |
| `fastn.state.get(key)` / `fastn.state.set(key, value)` | Durable key-value across runs. Scopes: `ORG`, `INVOCATION`.                                                         |
| `fastn.secrets.get("NAME")`                            | Read an encrypted secret. Name is UPPER\_SNAKE\_CASE, exactly as created. JSON-typed secrets return already parsed. |
| `fastn.envConfig.get("KEY")`                           | Read a per-environment config value.                                                                                |
| `fastn.diff.compare(…)`                                | Produce a sync report. A workflow that does not call this produces no report.                                       |

### Execution tiers

| Tier         | Caller gets                        | Ceiling    |
| ------------ | ---------------------------------- | ---------- |
| **Instant**  | Return value inline, synchronously | 30 seconds |
| **Standard** | `202` + execution id               | 15 minutes |
| **Long**     | `202` + execution id               | 36 hours   |

Instant is the default. Anything calling two or three systems in sequence belongs on Standard.

***

## Multi-tenant headers

Sent on the request; readable at `ctx.headers`.

| Header                        | Carries                                                    |
| ----------------------------- | ---------------------------------------------------------- |
| `x-end-org-id`                | The customer the run acts for                              |
| `x-end-org-ref`               | Your own reference for that customer                       |
| `x-installation-id`           | Which installation of the integration this run belongs to  |
| `x-fastn-connections`         | The connections the run may use                            |
| `x-fastn-installation-config` | The configuration values that installation was set up with |

***

## HTTP API

Base URL is your deployment — production is `https://app.fastn.dev`. Each workflow's **API** tab shows the correct host for your workspace. Examples below use `YOUR_FASTN_HOST`.

### Auth

```
Authorization: Bearer fsk_live_<key>
```

| Header              | Live key            | Test key                                              |
| ------------------- | ------------------- | ----------------------------------------------------- |
| `Authorization`     | `Bearer fsk_live_…` | `Bearer fsk_test_…`                                   |
| `X-fastn-Test-Mode` | Not needed          | **Must be `true`** — a test key is refused without it |
| `x-fastn-env`       | Environment slug    | Environment slug                                      |

`x-fastn-env: test` runs the latest published version; any other slug runs the version deployed to that environment.

{% hint style="danger" %}
A test key is **not** a sandbox. It reaches the same live connections as a live key and causes the same real writes. Never present test mode to a user as safe.
{% endhint %}

### Endpoints

```http
POST /api/v1/workflows/{workflowId}/execute

GET  /api/v1/unified/{category}/{entity}?page_size=50
GET  /api/v1/unified/{category}/{entity}/{recordId}
POST /api/v1/unified/{category}/{entity}

POST /api/v1/embed/token
POST /api/v1/embed/token/refresh
GET  /api/v1/embed/iframe?token=emb_…
```

Workflow ids look like `wf_b5880b29eb25`; execution ids like `exec_…`; embed tokens like `emb_…`.

Execute request body — the `input` object becomes `ctx.input`:

```json
{ "input": { "key": "value" } }
```

### Unified API categories

Five on the live platform: `CRM`, `Documents`, `Knowledge Base`, `Messaging`, `Project Management`.

CRM exposes ten entities across twelve providers: `Account`, `Contact`, `Engagement`, `Engagement Type`, `Lead`, `Note`, `Opportunity`, `Stage`, `Task`, `User`. Note and message entities are create-only.

### Errors

| Response                 | Means                                                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `WORKFLOW_NOT_PUBLISHED` | No snapshot has ever been published. **Every call returns this until one is.** The single most common cause of "the API did nothing". |
| `401` / `403`            | Key wrong, revoked, expired, outside its IP allowlist, or lacking the permission.                                                     |
| Test key rejected        | `X-fastn-Test-Mode: true` was not sent with a test key.                                                                               |

A workflow that runs and throws is a **Failed execution**, not a transport error. Look in Executions, not the HTTP response.

There is **no documented endpoint for polling an execution id**. For Standard and Long runs, have the workflow call back rather than assuming you can poll.

***

## Triggers

| Type          | Starts a run when                       | Key settings                                                                                           |
| ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Webhook**   | An HTTP request arrives at a public URL | Auth (`x-fastn-access-key` or public), routes, execution mode (Parallel/Sequential), deduplication key |
| **Schedule**  | A time is reached                       | Cron/interval, plus a **Payload** JSON that becomes `ctx.input`                                        |
| **App event** | Something changes in a connected system | Connector (**immutable after creation**), connection, event, routes                                    |

An app event trigger cannot be created without an active connection for that connector.

***

## Two different MCP surfaces

Do not conflate these.

|             | Docs MCP                                | Product MCP gateway                            |
| ----------- | --------------------------------------- | ---------------------------------------------- |
| **URL**     | `<docs-site>/~gitbook/mcp`              | `https://mcp.fastn.dev`                        |
| **Exposes** | This documentation, as searchable tools | Your connectors and actions, as callable tools |
| **Auth**    | Public, as the docs site is             | `Authorization: Bearer fsk_live_<key>`         |
| **Use for** | Answering questions about fastn         | Acting on real customer systems                |

Claude Code, against the product gateway:

```
claude mcp add --transport http fastn https://mcp.fastn.dev
```

A gateway client acts with exactly what its key permits — customer scope, permission preset, and the actions selected on the connector. Mint a dedicated key per client and name it after the client; the name appears in the audit log beside everything the key does.

***

## Things agents get wrong

Read this section before generating code.

* **`fastn.connector` vs `fastn.connectors`.** The product is inconsistent. The workflow **Docs** tab documents the singular; the **Connectors** tab describes extracting bound connectors from `fastn.connectors.X.Y(…)` calls on save. Check the Docs tab in the target workspace, and confirm saving actually picks the calls up. **Verify.**
* **`fastn.state`** **`ORG` scope is not pinned down.** Whether `ORG` is org-wide across all workflows or partitioned per workflow is not settled. Namespace keys by workflow (`myworkflow:deal:123`) so it does not matter. **Verify.**
* **`fastn.db` isolates by workspace, not by customer.** Every customer's rows share one schema. Nothing scopes a query for you — add a customer column and a predicate on every query, or you will leak across tenants.
* **Always parameterise SQL.** Interpolating `ctx.input` into a query string is an injection.
* **Publishing is not optional.** Code that is saved but not published does not run over the API.
* **Instant's 30 seconds is a hard ceiling**, and finding out by timing out in production is expensive.
* **Embed refresh caps at seven days per session.** At the cap the widget posts `fastn:session-expired` to the parent window and stops. Listen for it and mint a fresh token.
* **Connector credentials are not secrets.** They live on connections and are managed by fastn, including OAuth refresh. `fastn.secrets` is for third-party tokens and keys you hold yourself.
* **Settings is role-scoped.** What a user can see under Settings depends on their role, so do not assume a screen exists for the current user.

***

## Authoritative surfaces

Generated from the deployment you are actually calling, and therefore more trustworthy than any document — this one included:

| Surface                       | Gives you                                                                                    |
| ----------------------------- | -------------------------------------------------------------------------------------------- |
| Workflow → **Docs** tab       | Exact runtime call shapes and header value formats for your deployment                       |
| Workflow → **Contract** tab   | The real input and output shapes                                                             |
| Workflow → **API** tab        | A ready-to-run curl with your host, workflow id and contract fields                          |
| Workflow → **Connectors** tab | Which connectors and action versions are actually bound, and per-customer vs workspace scope |
| Connector detail page         | The action list, and the **Select all** control that scopes them                             |

When this page and a generated tab disagree, the tab is right.

***

## Machine-readable formats

This documentation is published from GitBook, which serves it in agent-friendly formats without configuration:

| Path                | Contains                                      |
| ------------------- | --------------------------------------------- |
| `/llms.txt`         | A curated index of the documentation          |
| `/llms-full.txt`    | The entire documentation as one markdown file |
| `<any page URL>.md` | That page as raw markdown                     |
| `/~gitbook/mcp`     | The documentation as an MCP server            |

Prefer `llms-full.txt` for a single bulk load, the MCP server for interactive lookup.


# FAQs

Short answers to the questions that come up most.

## Getting started

<details>

<summary>Do my customers need a fastn account?</summary>

No. Your customers work through the [widget](/embed/embed) embedded in your product, authorising their own accounts there. They reach it with an embed token your backend mints, which carries the role `end_user` — not a fastn login of their own.

</details>

<details>

<summary>Do I have to write workflow code?</summary>

No. The [agent](/build/agent) writes workflows from a plain description, and iterates on follow-up messages. In fact code editing is switched off in almost every workspace — it is enabled only for the parent organisation — so writing the code yourself is the exception rather than the norm. You can still test, wire connectors, edit the contract, publish and deploy. It can be switched on if you want to write code yourself; ask fastn to enable it.

</details>

<details>

<summary>What is the difference between a customer and a tenant?</summary>

They are the same thing under two names, and **both are current**. *Customer* is what the dashboard calls it — the Customers screen, the ⌘K search group, the Connections column. *Tenant* is what the plumbing calls it: it is the column header on all three Triggers tables, and the last segment of a connection id, `ucl:org_<org>:<env>:<connectorId>:<authId>:<tenant>`.

So do not read *tenant* as legacy vocabulary you can ignore. If a screen or an identifier says tenant, it means one of your customers.

</details>

***

## Connectors and connections

<details>

<summary>What is the difference between a connector and a connection?</summary>

A **connector** is the definition of a system — its actions, auth methods and webhooks. A **connection** is one customer's authorised link to it, holding the encrypted credential. One connector, many connections.

</details>

<details>

<summary>My customers see "fastn.ai" on the OAuth consent screen. How do I show my own brand?</summary>

Start on the connector's **Auth** tab, which is where a connector's OAuth providers are configured — it shows the auth methods and a providers list. Whether that lets you register your own OAuth application, and what the consent screen then shows, is not something this page can confirm; check the tab for your connector, and ask fastn if it is not there. See [Connectors](/build/connectors).

</details>

<details>

<summary>A connection says Expired or Failed. Can I fix it from the dashboard?</summary>

Not by re-entering the credential — it belongs to the customer, and they re-authorise through your widget. What the dashboard offers on the row is **Reconnect** and **Disconnect**.

The precise meaning of each status is not documented here; treat *Expired* and *Failed* as "this credential no longer works, ask the customer to reconnect" and read the connection's own **Token and activity** section for `Expires`, `Last refreshed` and `Last used`.

{% hint style="warning" %}
The **Active** / **Inactive** / **Expired** / **Failed** filter chips on the Connections tab currently return zero rows whichever one you pick, even when every row in the unfiltered list shows Active. Do not conclude from an empty filtered list that you have no connections in that state — clear the filter and read the Status column.
{% endhint %}

</details>

<details>

<summary>What is a workspace connection for?</summary>

Systems your organisation owns rather than your customers — your Slack, your warehouse. In a workflow, each connector is wired either **per customer** or **workspace**, which decides whose credential the call uses.

</details>

<details>

<summary>Can I stop one customer taking a connector change?</summary>

Yes. Pin them to a specific connector version under **Version pins** on the connector. Everyone else moves forward.

</details>

***

## Workflows and triggers

<details>

<summary>Which execution tier should I pick?</summary>

| Tier         | Behaviour                                     | Maximum        | Timeout slider |
| ------------ | --------------------------------------------- | -------------- | -------------- |
| **Instant**  | The caller blocks and gets the result inline. | **30 seconds** | 1s – 30s       |
| **Standard** | Returns 202, runs in the background.          | **15 minutes** | 5s – 15min     |
| **Long**     | Returns 202, runs in the background.          | **36 hours**   | 30s – 36h      |

**Instant** only when something is genuinely waiting on the answer — 30 seconds is not much once you are calling two systems in sequence. **Standard** for almost everything else. **Long** for batch imports and backfills.

</details>

<details>

<summary>What is the difference between Publish and Deploy?</summary>

**Publish** creates an immutable version snapshot (v1, v2, …). **Deploy** sends a published version to an environment so it starts handling real events. Rollback is just deploying an earlier version.

</details>

<details>

<summary>What does `test` mean in the environment dropdown?</summary>

`test` runs the workflow's latest published version. Any named environment runs the version deployed *there*, and the fire fails if nothing is deployed to it.

</details>

<details>

<summary>How do I stop duplicate records after a replay?</summary>

Set a **deduplication key** on the webhook trigger, and add an idempotency guard using `fastn.state` in the workflow. See [Common patterns](/build/patterns).

</details>

<details>

<summary>Does a retry policy retry everything?</summary>

No. It retries transient failures. **Code errors, data errors and out-of-memory never retry** — a bug does not get better on the second attempt, and neither does a payload that was always malformed.

</details>

<details>

<summary>I called the API and nothing ran.</summary>

Check whether the workflow has ever been published. Until a snapshot exists, every call returns `WORKFLOW_NOT_PUBLISHED` — the workflow list shows this as status **Not published** and latest version **Unpublished**. See [HTTP API](/reference/api).

</details>

***

## Security and access

<details>

<summary>Is a test API key a sandbox?</summary>

No. A test key reaches the same live connections as a live key and causes the same real writes. It is a separate, separately revocable credential — not a safe one. It is also refused unless the caller sends `X-fastn-Test-Mode: true`.

</details>

<details>

<summary>Can I put an API key in my frontend?</summary>

No. Browser-facing widgets use short-lived embed tokens minted by your backend. See [Embedding the widget](/embed/embedding).

</details>

<details>

<summary>Someone left the team. What do I need to do?</summary>

Remove them under [People](/manage/people) — their audit history stays. Then check [API keys](/manage/api-keys): keys belong to the workspace, not to a person, so removing someone does not revoke keys they created.

</details>

<details>

<summary>Who can read the audit log?</summary>

Account owners and admins only. It is enforced at the API layer and cannot be granted per user.

</details>

<details>

<summary>Can I limit an agent to read-only access for one customer?</summary>

Yes. Combine the action selection on the connector with a narrow API key: pick the **Viewer** preset, or **Custom** with only the read boxes ticked in the `What it can touch` matrix, and set **Customers it can reach** to **Only the ones I pick**. See [API keys](/manage/api-keys).

</details>

***

## Data and limits

<details>

<summary>Where does `fastn.db` data actually live?</summary>

In fastn's shared managed Postgres by default, in a schema isolated to your workspace. You can point it at a Postgres you operate instead, under [Settings → Database](/manage/database). Rows are never copied between the two, so switching changes where new reads and writes go and nothing else.

</details>

<details>

<summary>Secret or config?</summary>

Secret if exposure would be an incident — tokens, passwords, keys. Config if you would happily show it to a colleague — endpoints, feature flags, batch sizes. When in doubt, secret.

</details>

<details>

<summary>What happens when I hit a plan limit?</summary>

New work stops rather than being charged for, and nothing already running is interrupted. Check [Billing](/manage/billing) — a sync that stops because of a quota looks exactly like a broken sync until you do.

</details>

<details>

<summary>I deleted something by mistake.</summary>

Connectors, connector actions and workflows are in [Trash](/manage/trash) and restore with slug and history intact. Other resources — widgets and their integrations among them — are deleted immediately and cannot be restored from that page.

</details>

***

## Things that look broken

<details>

<summary>Why does the same connector appear twice in the catalogue?</summary>

Because two entries exist for one system — typically one badged **managed** and one badged **Custom**. Asana, HubSpot, Salesforce, Slack, Notion and Cin7 Core all show up this way.

The practical consequence: the connector count is a count of *entries*, not of distinct systems, so "354 connectors" is not 354 different products. Check the badge and the provenance line before you connect, so you do not authorise the copy you did not mean.

</details>

<details>

<summary>A connector says Connected but its detail page says 0 connections.</summary>

That is a known inconsistency between the list badge and the detail page's count, not a lost connection. Confirm on the [Connections](/build/connections) tab, which lists actual connections with their customer and status.

</details>

<details>

<summary>A connector shows "Created: Invalid Date".</summary>

A display defect in the Overview tab's Created row. It says nothing about the connector's health.

</details>

<details>

<summary>The Trash Actions tab never finishes loading.</summary>

Known: the **Actions** tab on [Trash](/manage/trash) sits on *Loading deleted actions…* and does not resolve. The Connectors and Workflows tabs work normally.

</details>

<details>

<summary>The Connections status filters return nothing.</summary>

Also known — every status chip returns zero rows. Clear the filter and read the Status column instead.

</details>

***

## Still stuck

Work through [Troubleshooting](/operate/troubleshooting) — it is organised by symptom.


# Glossary

Alphabetized definitions of every term used across the Fastn platform and documentation.

| Term                        | Definition                                                                                                                                                           | Where to find it                      |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| **Connector**               | A pre-built integration with a third-party system (Salesforce, Slack, Shopify, etc.). Handles authentication and exposes that system's actions.                      | Integrations → Connectors             |
| **Connection**              | An authenticated instance of a connector for a specific account. One connector can have multiple connections (e.g., two Slack workspaces).                           | Integrations → Connections            |
| **Workflow**                | An automation that moves and transforms data between systems. Built by AI agents from a plain-language description; the generated code can be inspected.             | Integrations → Workflows              |
| **Trigger**                 | What starts a workflow. Three types: Webhook, Scheduler, App Event.                                                                                                  | Integrations → Triggers               |
| **Customer**                | An end user organization of your SaaS product. Each customer's connections and data are isolated.                                                                    | Settings → Customers                  |
| **Widget**                  | The embedded interface your customers use to connect apps and manage integrations inside your product.                                                               | Widgets → Widget Builder              |
| **Setup Assistant**         | The AI-guided onboarding wizard that configures a new SaaS partner's integration platform (Use cases → Connectors → Workflows → Embed → Live).                       | Home page                             |
| **Build with AI**           | The label for the AI integration/workflow builder.                                                                                                                   | Workflows tab; embedded widget        |
| **Agent**                   | The Integrations sub-tab where you describe an integration and the AI agents build it.                                                                               | Integrations → Agent                  |
| **AI Assistant**            | A toggleable widget section that lets your customers describe an automation and have it built inside the widget.                                                     | Widget sections                       |
| **MCP Gateway**             | The endpoint that exposes integrations as tools for AI agents, over the Model Context Protocol. Has a Control Plane (Admin MCP) and a Data Plane (Customer Gateway). | Widgets → Embed → MCP                 |
| **Embed token**             | A short-lived token (15-minute expiry, `emb_` prefix) generated from your backend that scopes an embedded widget to a specific customer.                             | Generated via the embed token API     |
| **API key**                 | A credential in `fsk_` format (`fsk_test_` or `fsk_live_`) used to authenticate backend API calls, including generating embed tokens.                                | Settings → API Keys                   |
| **Execution tier**          | A workflow's runtime mode: Instant (synchronous, ≤60s), Standard (async, ≤15min), or Long (async, ≤6hr).                                                             | Workflow editor → Configuration       |
| **Dead Letter Queue (DLQ)** | An option on webhook triggers that captures failed events for later inspection or replay.                                                                            | Triggers → Webhook → Advanced options |
| **Org-level / User-level**  | The two ways a connection can be scoped: shared across a customer's whole organization (org-level) or isolated to an individual user (user-level).                   | Set via the embed iframe URL          |


# Fastn Classic

V1 is in maintenance mode. It will be supported until sunset. New features are only available in V2.

This section contains documentation for **Fastn V1** — the original embedded integration platform. V1 is currently in **maintenance mode**: active bugs are addressed, but no new features will be added. All new development is happening in [Fastn V2](https://claude.ai/README.md).

If you are starting a new project, use V2.

### What is V1?

Fastn V1 is a UI-driven embedded integration platform that lets SaaS companies give their customers pre-built connectors and automation flows. You configure connectors, build flows using a visual step editor, and embed an integration widget into your product.

V1 does not include the Canonical Data Model, TypeScript DSL, AI agents, or the MCP Gateway. These are V2-only capabilities.

### V1 vs V2 at a Glance

|                    | V1 (Legacy)                | V2                                                  |
| ------------------ | -------------------------- | --------------------------------------------------- |
| Workflow authoring | Visual step editor         | TypeScript DSL + visual canvas                      |
| Connector building | UI-only                    | Code-first (`ConnectorDefinition`)                  |
| AI agents          | Not available              | 7 platform agents + custom Agent Builder            |
| Data model         | Connector-specific schemas | Canonical Data Model (CDM)                          |
| Auth               | OAuth2, API Key            | OAuth2, API Key, SSO (Keycloak)                     |
| Multi-tenancy      | Single-tier                | Three-tier (Platform Admin / SaaS Admin / End User) |
| Status             | **Maintenance only**       | **Active development**                              |

### Support Timeline

V1 will be supported until the official sunset date is announced. When sunset is confirmed, this section will be archived and a migration guide will be published.

If you are on V1 and want to understand what moving to V2 involves, see the [Migration Guide](https://claude.ai/resources/migration-guide.md).

{% hint style="info" %}
⚠️ No new features will be added to V1. If a capability you need is not in V1, it will not be backported. Check the V2 docs to see if it is available there. {% endhint %}
{% endhint %}

### Known Limitations in V1

These gaps are documented and will not be fixed. They are resolved in V2.

**Sync & Scheduling**

* Scheduled sync cadence (daily / weekly / monthly / quarterly) is not available for folder-based file connectors. Only manual and event-based modes exist.
* Scheduler triggers can occasionally fail to pick up jobs, causing missed data runs.

**Connector & Flow Building**

* OAuth setup for custom connectors requires steps that are not fully surfaced in the UI. Expect to need support assistance the first time.
* Step-level test harness is not available. You cannot test a single connector step in isolation — you have to run the full flow.
* Copy-paste of flow steps can fail intermittently and requires a page reload to recover.
* Simple conditional logic (null checks, default values) requires multiple flow steps. There is no shorthand.

**Configuration Discoverability**

* Configuration dependencies — for example, linking a flow to a widget action — are not surfaced in the UI. You have to know to go to **Settings → Configurations** to find them.
* Some setup options are hidden behind toggles that are not labelled clearly. If something is not working, check for a collapsed or inactive toggle before raising a support ticket.

**Logs & Debugging**

* Execution logs show run time and return value only. Data passed between steps is not visible. You cannot inspect intermediate state without adding explicit output steps to your flow.
* Log search does not consistently return results when filtering by partial tenant ID. Use the full ID where possible.

**Editor Stability**

* The SQL and custom code editors do not retain unsaved state if you lose focus or navigate away. Save frequently.

### Finding V1 Documentation

Use the navigation under this **Legacy** section. The structure covers:

* **Getting Started** — account setup, first connector, first flow
* **Embedded Integrations** — connectors, flows, widget embedding
* **Tutorials & Resources** — step-by-step walkthroughs

{% hint style="info" %}
⚠️ **You are reading legacy documentation.** Features, UI labels, and configuration steps described here apply to V1 only. Do not mix V1 and V2 concepts the architectures are fundamentally different. If you are unsure which version you are on, check your dashboard URL or contact your Fastn account manager.
{% endhint %}


# Fastn V1: embedded integration platform for AI agents

Customers expect your product to work seamlessly with the tools they already rely on. Fastn makes it easy to deliver native integrations that connect with your users' apps, without the overhead of building and maintaining complex backend infrastructure.

Fastn provides access to over 15000+ tools through its extensive library of connectors, enabling your AI agents to automate and orchestrate workflows across a vast ecosystem of SaaS apps and services.

**With Fastn, you get:**

* **A fully branded integration portal**\
  Let your users connect and manage integrations directly inside your product with a polished, native experience.
* **Prebuilt and custom connectors**\
  Use Fastn's library of ready-to-go connectors or build your own to support any app your customers need.
* **Scalable multitenant architecture**\
  Support every customer with isolated environments, role-based access, and per-tenant customization out of the box.
* **Built-in monitoring and visibility**\
  Track integration activity, catch issues early, and get clear insights into how everything's performing.

### Integrate Instantly with 1000+ Connectors

*Connect Everything in Your Stack*

Fastn includes a growing library of[ 100&#x30;**+ Connectors**](https://fastn.ai/integrations) that are pre-built to unify your tech ecosystem. No more writing custom integration code; just plug, play, and automate.

<figure><img src="/files/tZYZThg3SEgox2rwteOw" alt="Fastn connector library showing 1000+ prebuilt integrations across popular apps and services"><figcaption></figcaption></figure>

New connectors are constantly being added via the Fastn Marketplace, where you can also find reusable widgets, logic modules, and third-party integrations ready to drop into your flows.

## Browse by Products

<details>

<summary><strong>UCL</strong></summary>

Securely connect your AI agents to the tools your users already rely on with enterprise scale in mind.

<figure><img src="/files/qRmddl9Sx2PHT9oz6TEe" alt="Animated demo of the Unified Context Layer connecting AI agents to user tools"><figcaption></figcaption></figure>

</details>

<details>

<summary><strong>Integration Hub</strong></summary>

An embedded app store that lets your users seamlessly connect and manage their apps directly inside your SaaS platform.

<figure><img src="/files/b34Z52DgXd7nIQHEU3hV" alt="Animated demo of the Integration Hub embedded app store for managing SaaS connections"><figcaption></figcaption></figure>

</details>

<details>

<summary><strong>AI Automation</strong></summary>

Build enterprise grade automations ten times faster using the power of generative AI.

<figure><img src="/files/XgNNUZZm8WBg4Fh4IJ8V" alt="Animated demo of AI Automation building enterprise-grade workflows with generative AI"><figcaption></figcaption></figure>

</details>

## Browse by Use Case

<details>

<summary><strong>Workflow Automation</strong></summary>

Fastn makes it easy to build and run automated workflows across your integrations. Whether you're syncing data, sending notifications, or triggering actions based on events, everything runs in the background, reliably and at scale.

An example is Fastn's **Zap Trigger Subscribe** to kick off a Zapier workflow whenever a new contact is added in your app. This lets your users instantly connect Fastn-powered actions with 5,000+ tools on Zapier, without writing a single line of code.

<div align="left"><figure><img src="/files/wBEPCkxGdsvfj0CTRz38" alt="Fastn flow showing a Zap Trigger Subscribe workflow for Zapier integration"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Event-Driven Triggers &#x26; Actions</strong></summary>

Once you've registered and set up your webhook routes in Fastn, you can configure triggers to automate when and how your workflows run.

Triggers let you schedule workflows to run automatically, at set intervals or specific times, without any manual input.

For example, you want to send a daily status update to your Slack channel every morning? Or maybe post a summary every 15 minutes? Fastn allows you to automate messages to Slack (or any connected app) based on your chosen schedule, so your team always stays in the loop.

<figure><img src="/files/O7G2nEM3wSWUIEr7VYyO" alt="Fastn flow with scheduled triggers sending automated messages to Slack on a recurring basis"><figcaption></figcaption></figure>

</details>

<details>

<summary><strong>Data Ingestion &#x26; Transformation</strong></summary>

Build reliable background pipelines that pull in your users' third-party data and update it in real time. With Fastn Flows, you can set up powerful, scalable automation

For example, automatically ingest Salesforce contacts and sync them into your users' HubSpot accounts as they update.

Fastn handles the heavy lifting so you can focus on your product, not data plumbing.

<div align="left" data-full-width="false"><figure><img src="/files/v366wZ04MKxZGfoP4jow" alt="Fastn flow ingesting Salesforce contacts and syncing them into HubSpot accounts"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Bi-Directional Sync</strong></summary>

Keep your product and your customers' tools in perfect sync with real-time, reliable two-way data flows.\
For example:

* Sync Salesforce contacts to HubSpot on a scheduled basis.
* Instantly push HubSpot contacts to Salesforce when new data is added.

Fastn makes two-way syncing seamless, whether it's time-based or triggered by events.

<div align="left"><figure><img src="/files/oRYJvvAnzgm84C6WnMoq" alt="Fastn flow syncing Salesforce contacts to HubSpot on a scheduled basis"><figcaption></figcaption></figure></div>

<div align="left"><figure><img src="/files/031ukFUZKeDNo3pMgRyl" alt="Fastn flow pushing HubSpot contacts to Salesforce when new data is added"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Agent-Based Tool Invocation</strong></summary>

Fastn lets AI agents trigger actions or fetch data from third-party tools using natural language.

For example, an agent can create a Google Doc, insert content, and share it—automatically, with no manual steps needed.

![AI agent creating a Google Doc, inserting content, and sharing it automatically](/files/bJMEjvAgAbKew5KpXPAn)

</details>

<details>

<summary><strong>Integration Monitoring &#x26; Alerting</strong></summary>

Fastn gives you real-time visibility into all your integrations and platform actions. Monitor which workflows are running smoothly, which ones are failing, and what automations are active. View latency trends over time, broken down by days, to quickly spot performance issues; all from one centralized dashboard.<br>

<figure><img src="/files/hKd6qOXO2hMfjA0nYuaX" alt="Fastn monitoring dashboard showing integration activity, latency trends, and workflow status"><figcaption></figcaption></figure>

</details>

Jump into **Your First Automation** to start building with Fastn.

{% content-ref url="<https://github.com/fastn-ai/docs/tree/main/your-first-automation.md>" %}
<https://github.com/fastn-ai/docs/tree/main/your-first-automation.md>
{% endcontent-ref %}


# Fastn: embedded integration platform for AI agents

Connect AI agents and SaaS applications to 250+ enterprise tools with Fastn's embedded integration platform. Managed OAuth, multi-tenant architecture, and sub-second execution.

Customers expect your product to work seamlessly with the tools they already rely on. Fastn makes it easy to deliver native integrations that connect with your users' apps, without the overhead of building and maintaining complex backend infrastructure.

Fastn provides access to over 15000+ tools through its extensive library of connectors, enabling your AI agents to automate and orchestrate workflows across a vast ecosystem of SaaS apps and services.

**With Fastn, you get:**

* **A fully branded integration portal**\
  Let your users connect and manage integrations directly inside your product with a polished, native experience.
* **Prebuilt and custom connectors**\
  Use Fastn's library of ready-to-go connectors or build your own to support any app your customers need.
* **Scalable multitenant architecture**\
  Support every customer with isolated environments, role-based access, and per-tenant customization out of the box.
* **Built-in monitoring and visibility**\
  Track integration activity, catch issues early, and get clear insights into how everything's performing.

### Integrate Instantly with 1000+ Connectors

*Connect Everything in Your Stack*

Fastn includes a growing library of[ 100&#x30;**+ Connectors**](https://fastn.ai/integrations) that are pre-built to unify your tech ecosystem. No more writing custom integration code; just plug, play, and automate.

<figure><img src="/files/nUmvKWf2ZV7xBOh8giw0" alt="Fastn connector library showing 1000+ prebuilt integrations across popular apps and services"><figcaption></figcaption></figure>

New connectors are constantly being added via the Fastn Marketplace, where you can also find reusable widgets, logic modules, and third-party integrations ready to drop into your flows.

## Browse by Products

<details>

<summary><strong>UCL</strong></summary>

Securely connect your AI agents to the tools your users already rely on with enterprise scale in mind.

<figure><img src="/files/qRmddl9Sx2PHT9oz6TEe" alt="Animated demo of the Unified Context Layer connecting AI agents to user tools"><figcaption></figcaption></figure>

</details>

<details>

<summary><strong>Integration Hub</strong></summary>

An embedded app store that lets your users seamlessly connect and manage their apps directly inside your SaaS platform.

<figure><img src="/files/b34Z52DgXd7nIQHEU3hV" alt="Animated demo of the Integration Hub embedded app store for managing SaaS connections"><figcaption></figcaption></figure>

</details>

<details>

<summary><strong>AI Automation</strong></summary>

Build enterprise grade automations ten times faster using the power of generative AI.

<figure><img src="/files/XgNNUZZm8WBg4Fh4IJ8V" alt="Animated demo of AI Automation building enterprise-grade workflows with generative AI"><figcaption></figcaption></figure>

</details>

## Browse by Use Case

<details>

<summary><strong>Workflow Automation</strong></summary>

Fastn makes it easy to build and run automated workflows across your integrations. Whether you're syncing data, sending notifications, or triggering actions based on events, everything runs in the background, reliably and at scale.

An example is Fastn's **Zap Trigger Subscribe** to kick off a Zapier workflow whenever a new contact is added in your app. This lets your users instantly connect Fastn-powered actions with 5,000+ tools on Zapier, without writing a single line of code.

<div align="left"><figure><img src="/files/Wg6lEMjOFj4TlQgDUsE4" alt="Fastn flow showing a Zap Trigger Subscribe workflow for Zapier integration"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Event-Driven Triggers &#x26; Actions</strong></summary>

Once you've registered and set up your webhook routes in Fastn, you can configure triggers to automate when and how your workflows run.

Triggers let you schedule workflows to run automatically, at set intervals or specific times, without any manual input.

For example, you want to send a daily status update to your Slack channel every morning? Or maybe post a summary every 15 minutes? Fastn allows you to automate messages to Slack (or any connected app) based on your chosen schedule, so your team always stays in the loop.

<figure><img src="/files/TFHuoVP6jkgL2UHMq2sk" alt="Fastn flow with scheduled triggers sending automated messages to Slack on a recurring basis"><figcaption></figcaption></figure>

</details>

<details>

<summary><strong>Data Ingestion &#x26; Transformation</strong></summary>

Build reliable background pipelines that pull in your users' third-party data and update it in real time. With Fastn Flows, you can set up powerful, scalable automation

For example, automatically ingest Salesforce contacts and sync them into your users' HubSpot accounts as they update.

Fastn handles the heavy lifting so you can focus on your product, not data plumbing.

<div align="left" data-full-width="false"><figure><img src="/files/3sUJae0nbsCmmaliD4Q5" alt="Fastn flow ingesting Salesforce contacts and syncing them into HubSpot accounts"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Bi-Directional Sync</strong></summary>

Keep your product and your customers' tools in perfect sync with real-time, reliable two-way data flows.\
For example:

* Sync Salesforce contacts to HubSpot on a scheduled basis.
* Instantly push HubSpot contacts to Salesforce when new data is added.

Fastn makes two-way syncing seamless, whether it's time-based or triggered by events.

<div align="left"><figure><img src="/files/7BIKGr5YbOHWgKYTSMnu" alt="Fastn flow syncing Salesforce contacts to HubSpot on a scheduled basis"><figcaption></figcaption></figure></div>

<div align="left"><figure><img src="/files/fyM1I03hRUuUVO183gJQ" alt="Fastn flow pushing HubSpot contacts to Salesforce when new data is added"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Agent-Based Tool Invocation</strong></summary>

Fastn lets AI agents trigger actions or fetch data from third-party tools using natural language.

For example, an agent can create a Google Doc, insert content, and share it—automatically, with no manual steps needed.

![AI agent creating a Google Doc, inserting content, and sharing it automatically](/files/cooawzZUvHKFVKnbX2Et)

</details>

<details>

<summary><strong>Integration Monitoring &#x26; Alerting</strong></summary>

Fastn gives you real-time visibility into all your integrations and platform actions. Monitor which workflows are running smoothly, which ones are failing, and what automations are active. View latency trends over time, broken down by days, to quickly spot performance issues; all from one centralized dashboard.<br>

<figure><img src="/files/YWkCDknwiz1V32kGuByQ" alt="Fastn monitoring dashboard showing integration activity, latency trends, and workflow status"><figcaption></figcaption></figure>

</details>

Jump into **Your First Automation** to start building with Fastn.

{% content-ref url="<https://github.com/fastn-ai/docs/tree/main/your-first-automation.md>" %}
<https://github.com/fastn-ai/docs/tree/main/your-first-automation.md>
{% endcontent-ref %}


# Embedded Integrations

These integrations are designed for your customers to use, no need for them to sign in to Fastn directly.

## **What Are Customer-Facing Integrations?**

Customer-facing integrations let you add Fastn-powered automations right into your own app, so your users can connect to tools like Shopify, Slack, or HubSpot without ever leaving your platform.

These integrations are designed for **your customers** to use; no need for them to sign in to Fastn directly.

## Why Built-In Integrations Matter

With such customer-facing integrations, you can:

* Make automation feel like a built-in part of your app.
* Let users connect their own accounts (e.g. Shopify stores) with one click.
* Run background workflows on their behalf, fast and securely.
* Deliver powerful features without building from scratch.

## **The Fastn Advantage**

* **No Backend Changes Required**\
  Integrate workflows without modifying your existing infrastructure.
* **Fully Customizable UI**\
  Match the look and feel of your app with flexible styling options.
* **Connect to Popular Apps**\
  Seamlessly integrate with platforms like Slack, Jira, Shopify, Gmail, and many more.
* **Enterprise-Grade Security**\
  Fastn manages authentication, permissions, and data flow securely and reliably

## **How It Works (Behind the Scenes)**

1. **Create a Flow**\
   Design powerful workflows in Fastn, such as exporting products, syncing data, or triggering updates across systems.
2. **Connect a Widget**\
   Attach your flow to a prebuilt, customizable UI widget ready to be embedded anywhere.
3. **Embed in Your App**\
   Drop the widget into your app using a simple script tag or iframe, no complex setup required.
4. **Let Users Take Action**\
   Your users authenticate securely, trigger flows directly from your app, and Fastn handles the rest.

## **Real-World Use Cases**

See how teams are using Fastn to deliver powerful integrations without the usual complexity:

<details>

<summary><strong>Shopify to Elasticsearch Sync - Built Into Your App</strong></summary>

You're building a storefront management tool. With Fastn, your users can connect their **Shopify store** in one click and instantly sync product listings to **Elasticsearch** for lightning-fast search—no APIs to wrangle or backend scripts to maintain.

> *Scenario:* A store owner updates a product title in Shopify → your app detects the change and updates the search index in real time.

</details>

<details>

<summary><strong>In-App Email Campaigns for CRM Users</strong></summary>

You're running a custom CRM. By embedding a Fastn-powered “Send Campaign” widget, your users can link their **Gmail or Mailchimp accounts**, segment contacts, and launch campaigns—all without ever leaving your dashboard.

> *Scenario:* A sales rep filters 500 leads → clicks “Email Segment” → campaign is launched via Gmail with tracked metrics—all in one flow.

</details>

<details>

<summary><strong>One-Click Data Export to Slack or Google Sheets</strong></summary>

Let your users export reports, alerts, or product data with a single click. Add buttons like **“Send to Slack”** or **“Export to Sheets”** right in your UI using Fastn widgets.

> *Scenario:* A product manager views a sales dashboard in your app → hits “Send to Slack” → a summary is instantly posted to #growth-team.

</details>

<details>

<summary><strong>Automated Inventory Sync Across Channels</strong></summary>

If you're building a multichannel inventory tool, use Fastn to let users connect platforms like **Shopify**, **Amazon**, and **NetSuite**—and keep inventory levels synced without them lifting a finger.

> *Scenario:* A merchant sells a product on Shopify → Fastn detects the sale → updates inventory across Amazon and NetSuite automatically.

</details>

<details>

<summary><strong>Sync Salesforce Deals to Your Analytics Dashboard</strong></summary>

If your product offers advanced analytics, Fastn can power a seamless Salesforce integration, so users can pull in live **deal data** with just a click.

> *Scenario:* A sales ops manager connects their Salesforce account → selects specific pipelines → within seconds, key metrics like win rates and deal velocity appear in their dashboard, refreshed daily.

</details>


# Getting Started with Fastn's Embedded Experience

Learn what the Embedded Experience is, where it's useful, and see how it works through a real-world Shopify example.

## What is the Embedded Experience?

The **Embedded Experience** in Fastn allows you to bring your automated workflows directly into your product or platform. This gives your users a seamless, interactive way to trigger and manage workflows, without needing to log into Fastn or understand its backend logic.

It’s designed to create a **white-labeled, native experience**, helping you deliver integrations and automation as a built-in feature of your own app.

## Where Embedding Is Useful

You can embed Fastn to:

* Let customers **trigger workflows** from within your app (e.g., sync product data, send alerts, or request reports).
* Allow users to **authenticate with third-party services** (like Shopify, Slack, or Jira) without leaving your platform.
* **Display workflow status or results** using your own UI components, keeping everything in one place.

Let’s walk through a real-world scenario of using Fastn’s embedded experience.

## Example Use Case: Sync Shopify Products to Elasticsearch

Imagine you're building a platform for e-commerce merchants, and you want to offer product sync from **Shopify to Elasticsearch,** without building the integration yourself.

Here’s how you can do it using Fastn:

* **Use a Prebuilt Template**
  * Go to the **Flows** page in Fastn.
  * Click on **Templates** in the top-right corner.
  * Switch to the **Community Templates** tab and select the **Shopify** label.
  * Find the **"Activate Shopify Export"** template, click the three dots, and select **Import**.
* **Activate with a Widget**
  * Once the flow is imported, attach it to a **Widget**.
  * Embed the widget in your app using a simple `<script>` or `<iframe>`.
  * Your users can now click a button to authenticate with Shopify and instantly enable the sync—without any dev work on your side.

This embedded flow will keep Elasticsearch updated automatically whenever new products are added to Shopify.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe1zf2HAGRryfoO-sczer2ui2jGMrwYeJGmDO7A5bJC8fP7gWzFhXo-kS18Z99CKhNjG55-Dqbqbrc0NTrfMbC_TaDkfQHWGDUPLvQt3T8SOmxb3Js9zqet15pWab1hnGKO1gd3?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Activate Shopify Export flow template showing the multi-step workflow for syncing Shopify products to Elasticsearch"><figcaption></figcaption></figure>

* The Activate Shopify Export flow will perform multiple actions. It will prep the Elasticsearch database by handling index creation and import our required flows such as the exporting Shopify products, updating Shopify products and searching for Shopify products in Elasticsearch. Additionally, it will create the required webhooks in fastn and Shopify allowing product updates to be sent.
* Once the flow is imported, it will show up on the Flows page. Next we are going to open the flow and update the projectId in the Variables step to match the current project id which can found in the URL as shown in the images below.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcPpJ1Yl6reT2cgjV2-gR7rb1Mp4Wp4P4sM13ejfa9IZ_BBCQq74rx9JQdunpjJ74iLzt_WMYLCGyG8YizZYBlpIMam1S4ihN-Z4RCDANAyLQSe4Fg_TgRKrTTY4Aeeyl5QwJTh?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Fastn Flows page showing the imported Activate Shopify Export flow"><figcaption></figcaption></figure>

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeny3xrP7sRYcRjXza7Hi-3JHzfEUbGh1h3tTloVjmf3TPnGfbTKFT5gnAwVnDt5VR1vkn-yowjgYaWqOG1SBylX3MDyx0WbNuykg6Uccl-Q7aCtV6ykatN5MBb10ESk_kCrV7wig?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Flow editor showing the Variables step with the projectId field highlighted for updating"><figcaption></figcaption></figure>

* Next we are going to deploy the flow by clicking on the Deploy button on the top right.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcjLeVTAf4FwW0tNClEDL4O-RjiIKPdfb5OfWJ48VYZuBAWiLLdW_3hVBZ4uk4PVN6luKRr9K4oGSYN20OQMka02Fiw7YEwraYS3SE5PmfribHDwrB66-WZgF2upHot9ghnhZrc0w?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Flow editor with the Deploy button highlighted in the top-right corner"><figcaption></figcaption></figure>

* Once the flow is deployed we are now ready to create our widget to trigger it.

### Setting up with Widget

* Go to the Connectors page, and find the Shopify connector under fastn connectors. Click on the three dots to see additional options and click on Publish.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdRNU3rmKsLSeWG1ojBo5FOMth70CYanwEWyU3CtKlP5ITeSG0ulXryAKOoMF1tYEFkmZABQPrhdbgoZ-SkV-kYRWEg1gf6RyiCILFEz6xDa06wSQ4H_gGbUMXL1dTgsIdoalTdBQ?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Connectors page showing the Shopify connector with the three-dot menu open and Publish option visible"><figcaption></figcaption></figure>

* This will open the create a widget page with Shopify's info pre-filled. Next add the Activate Shopify Exports flow as an activate action as shown below and update the widget.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfiBPBSWm5YfeK0suaHhOWCK43NzWFsGdQRUTE-vi3unwalJP_vpZ1VWmF2BQ774E3O3ZwX0uxtruTyW-WZaAYwpD-H3w-6tyPg1nhUgVT_WqVmksCrGvW4ZmSOYEsecRHzN6u6?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Widget editor showing Shopify info pre-filled with the Activate Shopify Exports flow added as an activate action"><figcaption></figcaption></figure>

* After the update the widget will be published. The published widget can be viewed by going to the Widgets page.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeGRIM5PF6s6pZZae1plmkgBIqBnsJfl9q-SPfrvHjWXqdWK6ysUHPdXf6pI7Ny4HEZ5bTl_9shWBQ6JamRmRmaundRQU9SNKKmnTO4z_VkXW8wSEn3zlWE2xVLLwhpaB2_9XoNHA?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Widgets page displaying the published Shopify widget"><figcaption></figcaption></figure>

* Click on the Integrate button, generate a key and click preview to open the preview page and see the created widget.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdo999hGTWSYHW9BJVvE_M0h8dB1UQY8n1xSWaiedzdkSaXC2SYMyLmnUvQZ_FHIsSl3wgnzUXKeA8bg4D9zPHMcIBxZJRStBtvq0KVJjb_WDuDnaU79xVBmENPk8M8KS2Y__gTlQ?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Widget preview page showing the Shopify widget with an Activate button for end users"><figcaption></figcaption></figure>

### Activating the flow

* Clicking on the Activate button will trigger the authentication of Shopify connector. Which will ask for the store name and authenticate using oauth.
* Once the authentication is successful the activate flow will be triggered.
* This step will import the required flows, which can be seen deployed in the Flows page.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXevUIzanBmXx1YK02l67IQV4o2vIRGHgQ_XdH-sxUaC2rxyXPDsE7dbFUcDhCMHHphOG1xk6gRR9e87OZ3ioSCDdCfiac_G3cNoNfOVZJWw19UbMBOfYSi8WzyuAtzbMXm18jz7gA?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Flows page showing the imported Shopify flows deployed after activation, including export, update, and search flows"><figcaption></figcaption></figure>

* A webhook with the desired configurations, will be created, that can be seen on the Webhooks page

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdy8xHeol73JRs2fpGzrd6wgz5yL2SM7XDONMI_azXqJGyTtVNzLfD6q03reKaCpxIZrXRXQdkfIkV-VjtXsMsJz7fp-Ea8PT924bUsocJj1yg8lHttCSM6lUMd8yQsDqSTX8WCWw?key=aWFNWdC9I0x6b7wkwYQbDg" alt="Webhooks page displaying the auto-created webhook with Shopify product update configurations"><figcaption></figcaption></figure>

The example above demonstrates how widgets can be used to trigger multiple flows against a particular use case.

The Shopify widget can be integrated into a user's application to allow client to export data into their own Shopify accounts by using Widget to trigger authentication and connecting to their specific accounts.




---

[Next Page](/llms-full.txt/1)

