Skip to content

Dynamic Post-Connection Configuration for SaaS Integrations: Building Data-Driven Setup Flows Without Custom Code

Learn to build post-connection configuration UI for SaaS integrations - from copy-paste React examples to declarative platforms that eliminate per-integration code.

Uday Gajavalli Uday Gajavalli · · 24 min read
Dynamic Post-Connection Configuration for SaaS Integrations: Building Data-Driven Setup Flows Without Custom Code

You are sitting in the final review meeting for a six-figure enterprise contract. The prospect loves your core product. They finally clicked "Connect" on your shiny new Salesforce integration. The OAuth dance completes successfully. They are redirected back to your application, ready to sync their data.

Then, they hit a brick wall.

Your UI presents a blank text input asking for their "Salesforce Custom Object API Name" and a "Record Type ID." The user, a VP of Sales, has no idea what a Record Type ID is. They open a new tab to Google it, get distracted by a Slack message, and close the window.

Or perhaps your user just completed OAuth with Asana or Zendesk. The token exchange worked. The connection is live. And then your UI drops them onto a screen that says: "Please enter your Workspace ID and Tag IDs." They have zero idea what a Tag ID is. That integration is now functionally dead. Your integration activation metric just took another hit.

This scenario plays out thousands of times a day across the B2B SaaS ecosystem. Engineering teams spend weeks navigating archaic SOAP endpoints, handling bizarre rate limits, and normalizing data schemas. But if the customer-facing integration drops the user into a confusing, manual setup flow immediately after authentication, all that engineering effort is wasted.

Dynamic post-connection configuration is the practice of fetching live data from a connected third-party app—like Asana workspaces, Zendesk tags, or Salesforce record types—and rendering it as interactive setup forms (dropdowns, multi-selects) immediately after OAuth or API key authentication, without writing custom frontend code per integration. If your integration setup flow asks users to manually type raw IDs or API names into blank text inputs, you are bleeding activation.

Your integrations page and setup flow are revenue surfaces. When someone searches for your integration capabilities, your marketing gets them in the door. But the post-connection setup experience is what actually activates the account. This is the dynamic post-connection configuration problem, and it kills more integrations than bad APIs ever will. This guide covers why that happens, what the alternatives look like, and how to implement data-driven setup flows that actually convert.

The Integration Activation Crisis: Why Post-Connection Setup Kills Deals

Getting an OAuth token is not the finish line. It is barely the starting gun. If a user authenticates but fails to configure the integration, that integration is functionally dead.

Integration activation rate measures the percentage of customers who enable at least one integration within a SaaS product, providing a clear indicator of ecosystem adoption, product stickiness and the real-world value of partner-powered functionality. PartnerStack defines this activation rate as a critical indicator of real-world value. When users drop off during the setup phase, your activation rate plummets. High activation rates signal strong integration relevance, effective onboarding and clear value realization. Low activation rates may reveal friction in setup, weak integration discoverability or limited alignment between the integration and customer needs.

The business impact is measurable and blunt. According to Kissmetrics, users who complete onboarding retain at 2-3x the rate of those who do not. Steep drops often occur at specific, high-friction configuration steps. Asking a non-technical user to locate a 32-character alphanumeric ID hidden deep within their Zendesk admin settings is the definition of high friction.

And a 25% increase in activation results in a 34% rise in monthly recurring revenue (MRR) over 12 months. That is not a vanity metric. That is direct revenue tied to whether your user can successfully configure an integration after connecting.

The problem compounds when you consider that the ability to support this integration process is the number one sales-related factor in driving a software decision, according to analysis of Gartner's 2024 Global Software Buying Trends report. If the buyer gets through your sales process, signs the contract, and then hits a wall on integration setup, you have a churn time bomb.

If your team keeps hearing, "We connected the integration but it isn't syncing anything," you are bleeding pipeline and risking renewals. Broken or unusable integrations are a leading cause of churn. To reduce customer churn caused by broken integrations, you must fix the exact moment the user transitions from authenticating to configuring. Post-connection configuration is the gap between "connected" and "actually working." Most teams treat it as an afterthought. It should be treated as a revenue surface.

What is Dynamic Post-Connection Configuration?

Dynamic post-connection configuration is the process of fetching live data directly from a third-party API immediately after authentication to populate user-facing setup forms with valid, selectable options.

Instead of a blank text box asking for an ID, dynamic configuration presents a dropdown menu populated with real data. Your application uses the freshly acquired credentials to call that service's API, retrieve account-specific data, and present it in an interactive form—letting the user select their workspace, tags, record types, or other scoping parameters rather than typing raw identifiers.

The contrast with static configuration is stark:

Approach User Experience Error Rate Engineering Cost
Static text input "Enter your Workspace ID" High - typos, wrong IDs, wrong format Low upfront, high support cost
Static dropdown Pre-built list of options Medium - list goes stale Medium - requires maintenance
Dynamic dropdown Live options fetched from user's account Very low - only valid options shown Varies by approach

Documentation from integration platforms explicitly calls out dynamic dropdowns as input fields that fetch options from an API to reduce errors and offer valid, current choices. Dynamic dropdowns are input fields that fetch their list of options from an API or based on another field's input. Unlike static dropdowns, which show a fixed list, dynamic dropdowns update in real-time, offering a flexible and powerful way to present relevant options to users.

Examples of dynamic configuration in practice:

  • Asana: Fetching the /workspaces endpoint to populate a "Select Workspace" dropdown with human-readable names like "Engineering Team" instead of asking for 1234567890.
  • Zendesk: Querying the API for available tags and presenting a multi-select checklist so users can choose exactly which tickets to sync.
  • Salesforce: Pulling a live list of custom objects and record types so the user can select "Enterprise Deals" from a list.

Here is a concrete example. Say your product syncs tickets from Zendesk, and users want to allow users to sync only tickets that have certain tags. After the user connects their Zendesk account, a dynamic configuration form would:

  1. Call the Zendesk API using the user's fresh credentials to fetch their list of tags.
  2. Render those tags as a Multi Select field.
  3. Let the user pick the tags they care about.
  4. Store those selections as variables on the connected account context for use by downstream sync jobs.

The user never sees a raw API. They never Google "how to find my Zendesk tag ID." They pick from a list of their own data and move on.

sequenceDiagram
    participant User
    participant UI as Frontend UI
    participant Truto as Truto Engine
    participant API as Third-Party API
    
    User->>UI: Completes OAuth
    UI->>Truto: Request setup form configuration
    Truto->>API: Fetch live configuration data (e.g., /workspaces)
    API-->>Truto: Return native JSON response
    Note over Truto: Apply JSONata mapping<br>Extract IDs and Labels
    Truto-->>UI: Return normalized form schema
    UI-->>User: Render dynamic dropdowns

By presenting users with data they recognize, you eliminate the need for them to leave your app, search external documentation, and manually copy-paste IDs.

The Problem with Legacy Approaches to Dynamic Dropdowns

Building dynamic dropdowns sounds simple in theory. Make an API call, map the response to a list, and render a <select> element. The concept of dynamic dropdowns is not new. The problem is how existing tools implement them. Legacy integration platforms and in-house builds fail here because they rely heavily on custom code for every single provider.

Building In-House: The Per-Integration Code Trap

If you build dynamic post-connection configuration yourself, every integration becomes its own mini-project. For Asana, you write code to fetch workspaces. For Zendesk, you write code to fetch tags. For Salesforce, you write code to fetch record types. Each one needs its own:

  • API call logic: Injecting the proper authentication headers.
  • Pagination handling: What if the user has 500 tags? When an API changes its pagination cursor format from offset to page_token, your dropdown breaks, and you have to deploy a code fix.
  • Response parsing: Asana returns { data: [...] }, Zendesk returns { tags: [...] }, Salesforce returns { records: [...] }.
  • Error handling: Dealing with expired tokens, rate limits, or permission denied errors.
  • Frontend component wiring: Building custom React components for every setup requirement.

Multiply that by 30 or 50 integrations and your "quick setup form" has become a full-time engineering job.

Zapier's Developer Platform: Custom JavaScript Per Dropdown

Take the Zapier Developer Platform as an example. Zapier's developer platform supports dynamic dropdowns, but the approach requires developers to write custom JavaScript functions per integration.

Sometimes, API endpoints require clients to specify a parent object in order to create or access the child resources. For instance, specifying a spreadsheet id in order to retrieve its worksheets. Since people don't speak in auto-incremented IDs, it is necessary that Zapier offer a simple way to select that parent using human readable handles. Our solution is to present users a dropdown that is populated by making a live API call to fetch a list of parent objects.

The implementation requires writing custom JavaScript functions that make the API call, handle pagination, format the response into the exact {id, label} shape Zapier expects, and manage field dependencies. For each dropdown. For each integration. The Zapier community forums are full of developers struggling with dynamic dropdown configuration—questions about field dependencies, response formatting, and cascading dropdown behavior show up repeatedly.

This is not a criticism of Zapier—they solve a different problem. But if you are building customer-facing integrations for your own SaaS product, the per-integration JavaScript overhead defeats the purpose of reducing engineering work.

Embedded iPaaS Limitations: Visual Builders with Hidden Complexity

Embedded iPaaS solutions like Cyclr or Paragon provide visual workflow builders, which can speed up the creation of integration flows. But for post-connection configuration specifically, they often struggle with dynamic user-facing UI.

You often still need to manually map API responses to form fields, handle pagination for large option sets, and write custom logic when the third-party API returns data in an unusual structure. You end up building complex internal workflows just to return an array of strings to your frontend. The visual builder helps with orchestration, not with the fundamental data-to-dropdown mapping problem.

Building Post-Connection Configuration UI from Scratch

Architecture diagrams tell you what a post-connection setup flow should do. Code tells you what it costs. Before evaluating declarative platforms or embedded solutions, it helps to see the actual engineering surface area of building dynamic post-connection UI yourself. The React examples below are framework-specific, but the underlying complexity - pagination, rate limits, accessibility, state management, error recovery - is the same in any stack.

Prerequisites: Secure Token Handling

After the OAuth callback completes, your backend holds a fresh access token for the connected account. Rule one of post-connection UI: never expose raw third-party tokens to the browser. Your frontend should call your own backend, which proxies configuration requests to the third-party API using stored credentials.

// Your backend exposes a proxy endpoint for config data
// GET /api/connections/:id/config-data?resource=workspaces&cursor=...
//
// The backend:
// 1. Looks up the stored OAuth token for this connection
// 2. Calls the third-party API (e.g., Asana /workspaces)
// 3. Forwards the response and rate-limit headers to your frontend
 
async function fetchConfigData(connectionId, resource, cursor) {
  const params = new URLSearchParams({ resource });
  if (cursor) params.set('cursor', cursor);
 
  const res = await fetch(
    `/api/connections/${connectionId}/config-data?${params}`,
    { credentials: 'include' }
  );
 
  if (!res.ok) {
    const retryAfter = res.headers.get('ratelimit-reset');
    throw {
      status: res.status,
      retryAfter: retryAfter ? parseInt(retryAfter, 10) : null,
    };
  }
 
  return res.json();
}

This also means you need to build the backend proxy layer itself - handling token refresh, injecting provider-specific auth headers, and forwarding rate limit headers back to the frontend. That is real engineering work before you write a single line of UI code.

Fetching Data with Cursor Pagination and Rate Limit Handling

Most third-party APIs paginate their responses. A Zendesk account with hundreds of tags or an Asana org with dozens of workspaces will not return everything in one request. Here is a React hook that handles cursor-based pagination with automatic rate limit retries:

import { useState, useEffect, useCallback, useRef } from 'react';
 
function usePaginatedFetch(connectionId, resource) {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [cursor, setCursor] = useState(null);
  const [hasMore, setHasMore] = useState(true);
  const mountedRef = useRef(true);
 
  useEffect(() => () => { mountedRef.current = false; }, []);
 
  const fetchPage = useCallback(async (nextCursor) => {
    setLoading(true);
    setError(null);
 
    const params = new URLSearchParams({ resource });
    if (nextCursor) params.set('cursor', nextCursor);
 
    try {
      const res = await fetch(
        `/api/connections/${connectionId}/config-data?${params}`,
        { credentials: 'include' }
      );
 
      // Rate limit: read the standardized header and auto-retry
      if (res.status === 429) {
        const resetSec = parseInt(
          res.headers.get('ratelimit-reset') || '5',
          10
        );
        setError(`Rate limited by provider. Retrying in ${resetSec}s...`);
        setTimeout(() => {
          if (mountedRef.current) fetchPage(nextCursor);
        }, resetSec * 1000);
        return;
      }
 
      if (!res.ok) {
        throw new Error(
          `Failed to load ${resource} (HTTP ${res.status})`
        );
      }
 
      const json = await res.json();
      if (!mountedRef.current) return;
 
      setItems((prev) => [...prev, ...json.data]);
      setCursor(json.next_cursor ?? null);
      setHasMore(!!json.next_cursor);
    } catch (err) {
      if (mountedRef.current) setError(err.message);
    } finally {
      if (mountedRef.current) setLoading(false);
    }
  }, [connectionId, resource]);
 
  useEffect(() => {
    fetchPage(null);
  }, [fetchPage]);
 
  const loadMore = useCallback(() => {
    if (hasMore && !loading) fetchPage(cursor);
  }, [hasMore, loading, cursor, fetchPage]);
 
  return { items, loading, error, hasMore, loadMore };
}

Key design decisions in this hook:

  • Rate limit retries: When the API returns 429, the hook reads the ratelimit-reset header for the exact wait time, displays a status message, and retries automatically. No silent failures or hanging spinners.
  • Cursor accumulation: Each page appends to the existing items array. The hasMore flag tells the UI whether to offer a "load more" trigger.
  • Cleanup guard: The mountedRef check prevents React state updates after component unmount - a common source of bugs in async data fetching flows.

Rendering a Searchable Dropdown with Keyboard Navigation

A native <select> element falls apart once the option list exceeds a few dozen items. Users need type-ahead filtering, keyboard navigation, and screen reader support. A combobox is an input widget with an associated popup that enables users to select a value for the combobox from a collection of possible values. Here is a searchable dropdown following the WAI-ARIA combobox pattern:

import { useState, useRef, useEffect } from 'react';
 
function SearchableDropdown({
  items,
  loading,
  error,
  selected,
  onSelect,
  label,
  placeholder = 'Search...',
  hasMore,
  onLoadMore,
}) {
  const [query, setQuery] = useState('');
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const listRef = useRef(null);
  const listboxId = `listbox-${label.replace(/\s+/g, '-').toLowerCase()}`;
 
  const filtered = items.filter((item) =>
    item.label.toLowerCase().includes(query.toLowerCase())
  );
 
  // Keep the active option visible when navigating with arrow keys
  useEffect(() => {
    if (activeIndex >= 0 && listRef.current) {
      const el = listRef.current.children[activeIndex];
      el?.scrollIntoView({ block: 'nearest' });
    }
  }, [activeIndex]);
 
  function handleKeyDown(e) {
    if (!isOpen && (e.key === 'ArrowDown' || e.key === 'Enter')) {
      setIsOpen(true);
      e.preventDefault();
      return;
    }
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex((i) => Math.max(i - 1, 0));
        break;
      case 'Enter':
        e.preventDefault();
        if (activeIndex >= 0 && filtered[activeIndex]) {
          onSelect(filtered[activeIndex]);
          setIsOpen(false);
          setQuery('');
        }
        break;
      case 'Escape':
        setIsOpen(false);
        setActiveIndex(-1);
        break;
    }
  }
 
  // Infinite scroll: load next page near the bottom of the list
  function handleListScroll(e) {
    const el = e.target;
    if (
      hasMore &&
      el.scrollHeight - el.scrollTop - el.clientHeight < 50
    ) {
      onLoadMore?.();
    }
  }
 
  return (
    <div className="dropdown-wrapper">
      <label id={`${listboxId}-label`}>{label}</label>
      <div
        role="combobox"
        aria-expanded={isOpen}
        aria-haspopup="listbox"
        aria-owns={listboxId}
      >
        <input
          type="text"
          value={isOpen ? query : selected?.label || ''}
          placeholder={selected ? selected.label : placeholder}
          onChange={(e) => {
            setQuery(e.target.value);
            setIsOpen(true);
            setActiveIndex(-1);
          }}
          onFocus={() => setIsOpen(true)}
          onBlur={() => setTimeout(() => setIsOpen(false), 150)}
          onKeyDown={handleKeyDown}
          aria-autocomplete="list"
          aria-controls={listboxId}
          aria-activedescendant={
            activeIndex >= 0
              ? `${listboxId}-opt-${activeIndex}`
              : undefined
          }
          aria-labelledby={`${listboxId}-label`}
        />
      </div>
 
      {isOpen && (
        <ul
          ref={listRef}
          id={listboxId}
          role="listbox"
          aria-labelledby={`${listboxId}-label`}
          onScroll={handleListScroll}
          style={{ maxHeight: 240, overflowY: 'auto' }}
        >
          {error && (
            <li role="alert" className="dropdown-error">
              {error}
            </li>
          )}
          {filtered.map((item, i) => (
            <li
              key={item.value}
              id={`${listboxId}-opt-${i}`}
              role="option"
              aria-selected={selected?.value === item.value}
              className={i === activeIndex ? 'active' : ''}
              onMouseDown={() => {
                onSelect(item);
                setIsOpen(false);
                setQuery('');
              }}
            >
              {item.label}
            </li>
          ))}
          {loading && (
            <li className="dropdown-loading">Loading more...</li>
          )}
          {!loading && filtered.length === 0 && !error && (
            <li className="dropdown-empty">No results found</li>
          )}
        </ul>
      )}
    </div>
  );
}

That is roughly 100 lines for a single searchable dropdown. It does not yet handle multi-select, chip/tag rendering, "select all" logic, or mobile touch interactions. DOM focus is maintained on the combobox and the assistive technology focus is moved within the listbox using aria-activedescendant. Getting these ARIA semantics right matters - screen reader users depend on them to navigate your setup flow.

Wiring It Together: The Complete Setup Form

Now combine the paginated fetch hook with the dropdown component in a setup form that persists the user's selection:

function PostConnectionSetup({ connectionId }) {
  const {
    items: workspaces,
    loading,
    error,
    hasMore,
    loadMore,
  } = usePaginatedFetch(connectionId, 'workspaces');
 
  const [selected, setSelected] = useState(null);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState(null);
 
  async function persistSelection() {
    if (!selected) return;
    setSaving(true);
    setSaveError(null);
 
    try {
      const res = await fetch(
        `/api/connections/${connectionId}/context`,
        {
          method: 'PATCH',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            workspace_id: selected.value,
            workspace_name: selected.label,
          }),
        }
      );
      if (!res.ok) throw new Error('Failed to save configuration');
      // Redirect to success state or trigger first sync
      window.location.href = `/integrations/${connectionId}/active`;
    } catch (err) {
      setSaveError(err.message);
    } finally {
      setSaving(false);
    }
  }
 
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        persistSelection();
      }}
    >
      <h2>Configure Your Integration</h2>
 
      <SearchableDropdown
        items={workspaces.map((w) => ({
          label: w.name,
          value: w.gid,
        }))}
        loading={loading}
        error={error}
        selected={selected}
        onSelect={setSelected}
        label="Select Workspace"
        hasMore={hasMore}
        onLoadMore={loadMore}
      />
 
      {saveError && (
        <p role="alert" className="error">
          {saveError}
        </p>
      )}
 
      <button type="submit" disabled={!selected || saving}>
        {saving ? 'Saving...' : 'Continue'}
      </button>
    </form>
  );
}

Persisting Selections to the Connection Record

The PATCH call above writes the user's selections to the connection's context - a JSON object that holds all per-account configuration. Here is the expected API shape:

// PATCH /api/connections/conn_abc123/context
// Request body:
{
  "workspace_id": "12345",
  "workspace_name": "Engineering Team"
}
 
// Response:
{
  "id": "conn_abc123",
  "integration": "asana",
  "status": "configured",
  "context": {
    "workspace_id": "12345",
    "workspace_name": "Engineering Team"
  }
}

Once stored, downstream API calls reference these values through placeholders. An endpoint configured as /api/1.0/workspaces/{{context.workspace_id}}/projects automatically resolves to the correct workspace at request time. Your application code never needs to know which workspace the user picked - the platform handles the routing.

Accessibility and Error-State Handling

The dropdown component above implements the core WAI-ARIA combobox pattern, but a production post-connection UI needs a complete accessibility and error strategy.

Keyboard and screen reader checklist:

  • role="combobox" on the input wrapper with aria-expanded, aria-haspopup="listbox", and aria-owns
  • role="listbox" on the options container with role="option" on each item
  • aria-activedescendant tracks the currently highlighted option without moving DOM focus
  • aria-selected marks the chosen option
  • Arrow keys navigate the list, Enter selects, Escape closes
  • Focus returns to the input after selection
  • Visible focus indicator on the active option (do not rely on :hover alone)

Error states to handle in production:

Error User-Facing Behavior Implementation
Network failure Inline retry button with "Could not load options" message Catch in fetch, render retry action
401 / 403 Prompt the user to reconnect - token expired or lacks permissions Detect status, redirect to re-auth flow
429 rate limit Show countdown timer, auto-retry after reset window Read ratelimit-reset header (see hook above)
Empty response "No workspaces found in your account" with help link Check json.data.length === 0
Request timeout "Taking longer than expected" with retry option AbortController with 15-second timeout

Visual and interaction requirements:

  • Loading skeleton or spinner while options load - never show an empty dropdown that looks broken
  • Minimum touch target size of 44x44px for mobile users (WCAG 2.5.8)
  • Color contrast ratio of at least 4.5:1 for option text (WCAG AA)
  • Disabled submit button until a valid selection is made

The Real Cost of DIY Post-Connection UI

That was one searchable dropdown for one integration. In a production environment, you also need:

  • Multi-select fields for tag pickers and permission scopes - different selection model, chip rendering, "select all" logic, and distinct keyboard behavior
  • Cascading dropdowns where selecting a workspace triggers a project fetch - dependency management, per-field loading states, and cache invalidation
  • Token refresh mid-flow when the access token expires during configuration
  • Per-provider response mapping - Asana returns { data: [...] }, Zendesk returns { tags: [...] }, Salesforce returns { records: [...] }
  • Backend proxy layer that handles auth injection, pagination forwarding, and token refresh for every provider you support
  • Test coverage for each integration's response shape and edge cases

Multiply this by 30 or 50 integrations and the "simple setup form" has become a dedicated engineering workstream. This is exactly why declarative approaches exist - and why the per-integration code trap is worth avoiding from day one.

Building Data-Driven Setup Flows Without Custom Code

The alternative to writing per-integration code is a declarative approach: you describe what data to fetch and how to present it, and a generic engine handles the execution.

The best integration strategy for scaling B2B SaaS companies is shipping API connectors as data-only operations without writing integration-specific code. Truto achieves this through a declarative architecture powered by JSONata. JSONata is a functional query and transformation language for JSON. Think of it as a powerful expression language purpose-built for reshaping JSON objects.

Instead of writing a custom JavaScript handler for an Asana workspace dropdown and a separate handler for a Zendesk tags dropdown, you define the mapping as data. This pattern has three components:

  1. A data source declaration - "Call GET /workspaces on this integration and extract the data array"
  2. A transform expression - "Map each item to { label: item.name, value: item.gid }" using JSONata.
  3. A form field definition - "Render this as a Single Select field labeled 'Select Workspace'"

When the transform layer uses a general-purpose expression language rather than integration-specific code, you can add a new dynamic dropdown for a new integration without deploying anything. The expression is data, not code. The runtime engine does not know or care if it is parsing HubSpot properties or Salesforce objects. It simply executes the expression against the incoming JSON.

Consider what a declarative configuration for an Asana workspace selector might look like:

field:
  type: single_select
  label: "Select Workspace"
  key: "workspace_id"
  source:
    resource: workspaces
    method: list
  transform: |
    response.data.{
      "label": name,
      "value": gid
    }

No JavaScript. No pagination handler. No per-integration HTTP client. The platform knows how to call the Asana API (that is already defined in the integration configuration). The JSONata expression maps the response into label/value pairs. The field definition tells the UI what to render.

Want to add a Zendesk tag selector? Swap the resource and transform:

field:
  type: multi_select
  label: "Select Tags to Sync"
  key: "selected_tags"
  source:
    resource: tags
    method: list
  transform: |
    response.tags.{
      "label": name,
      "value": name
    }

Same engine. Different data. Zero custom code.

This is the same architectural principle behind the idea that integration behavior should be described as declarative configuration rather than hardcoded logic. It is the difference between writing an if (provider === 'asana') branch and adding a row to a configuration table. This architectural pattern is what allows teams to deliver the "Tuesday to Friday" integration, unblocking enterprise deals in days rather than quarters.

How Truto RapidForm Automates Post-Connection Configuration

Truto solves the post-connection drop-off problem directly with RapidForm. RapidForm is a powerful tool designed to collect input from end users as part of the integrated account connection flow. It streamlines the process of gathering necessary data and preferences immediately after a user connects their account.

Instead of forcing your engineering team to build custom React components for every integration's setup requirements, RapidForm automatically generates dynamic, user-facing setup forms. Because RapidForm is hosted and managed by Truto as part of the connection flow, your frontend engineers do not need to build custom setup screens. You do not need to maintain state, handle pagination for dropdown lists, or worry about rendering multi-select checkboxes for 50 different APIs.

Here is how it works at each step:

Step 1: Define the Form Fields and Authenticate

The user completes the standard OAuth flow via the Truto interface. Meanwhile, RapidForm fields are defined as configuration on the integration, not as frontend components. Each field specifies its type (Single Select, Multi Select, Text, Password, Checkbox, Hidden), its label, and optionally a data source.

For dynamic fields, the data source is a reference to an existing integration resource. When the form loads, RapidForm automatically calls that resource using the user's freshly authenticated credentials to fetch the live configuration data options.

Step 2: Transform the Response into Options

A declarative transform expression maps the raw API response into label/value pairs for the dropdown. This uses the JSONata expression language used across the rest of the platform's mapping layer—no per-integration JavaScript.

For example, fetching Asana workspaces and rendering them as a dropdown is a configuration change, not a code change. The platform already knows how to authenticate with Asana and paginate through its API. The transform expression just describes which fields to extract.

Step 3: Render and Collect User Input

The form renders automatically as part of the connection flow. Truto presents a clean UI with Single Select, Multi Select, Text, or Checkbox fields populated with the live data. The user sees a clean UI with populated dropdowns, makes their selections, and submits.

Step 4: The Power of the Integrated Account Context

The true advantage of RapidForm is what happens after the user submits the form.

When the user makes their selections and clicks save, those values are automatically stored as variables in the Integrated Account context—a generic JSON object that holds all per-account configuration. These variables are immediately available to subsequent API calls, sync jobs, and unified API requests.

Truto's proxy layer and unified API engine use placeholder syntax (via the @truto/replace-placeholders library) to dynamically inject these variables into subsequent API requests.

If a user selected a workspace with the ID 12345, that ID is stored as context.workspace_id. When your application makes a unified API call to list projects, the underlying integration configuration can define the endpoint path as /api/1.0/workspaces/{{context.workspace_id}}/projects.

Truto automatically replaces the placeholder with the user's selected value at runtime. Your application logic remains completely clean. You just call GET /unified/projects, and Truto handles routing the request to the exact workspace the user selected during onboarding. The values flow through the entire system without any glue code.

What About Rate Limits When Fetching Configuration Data?

When you fetch live data for a dropdown, you are entirely at the mercy of the third-party API's rate limits. If a user clicks "Connect" right as their organization hits its HubSpot API quota, your dynamic dropdown request will fail with an HTTP 429 error.

Many integration platforms try to magically absorb these errors, which leads to silent failures and hanging UI spinners. Fetching dropdown options means making API calls to the third-party during the setup flow. If the third-party rate-limits those calls, you need to handle it transparently.

Truto takes a radically transparent approach to rate limits. Truto does not automatically retry, throttle, or apply backoff on rate limit errors. If the upstream API returns a 429, that error passes through to your application.

What Truto does do is normalize the chaotic rate limit headers from hundreds of APIs into standardized response headers based on the IETF RateLimit specification:

  • ratelimit-limit: The maximum number of requests permitted in the current window.
  • ratelimit-remaining: The number of requests remaining in the current window.
  • ratelimit-reset: The number of seconds until the rate limit window resets.

This gives your application consistent rate limit data regardless of which provider you are calling. Your frontend or backend agent is responsible for reading these standardized headers and implementing its own retry or backoff logic—perhaps showing a "please wait" message and retrying after the reset window. This ensures you have total control over the user experience when a dropdown fails to load, without having to learn each provider's proprietary rate limit response format.

Cascading Dropdowns and Conditional Fields

Real-world configuration often has dependencies. The user selects a Workspace, then the form fetches Projects within that workspace. RapidForm supports this through field dependencies—selecting a value in one field triggers a fresh API call for the dependent field, scoped to the selected parent.

This is the same pattern Zapier calls altersDynamicFields, but without the JavaScript. The dependency is declared in the field configuration.

Custom Validation

RapidForm supports custom validation logic through transform expressions. After the user submits, a validation expression can check the selections against business rules—ensuring the selected workspace has the right permissions, or that the chosen tags meet a minimum count. If validation fails, the user sees a clear error message before the connection is finalized.

Measuring the Impact: Before and After

The business case for dynamic post-connection configuration is straightforward to measure. Track these metrics before and after implementing it:

  • Step completion rate: What percentage of users who start the configuration form finish it? The most fundamental onboarding metric is the completion rate for each step: what percentage of users who start a given step finish it? This metric reveals your onboarding funnel's shape and highlights the specific steps where users get stuck.
  • Time to first sync: How long between OAuth completion and the first successful data sync?
  • Support ticket volume: How many tickets reference "connection setup" or "configuration help"?
  • Integration activation rate: What percentage of users who start connecting actually get to a working integration?

Upfront payment requirements, excessive form fields, and complex configuration steps introduce friction before users can see any value. Each additional form field reduces completion by an average of 5-7%. Replacing three text inputs with one dynamic dropdown is not just a UX improvement. It is potentially a 15-20% improvement in step completion.

One real-world datapoint from the B2B SaaS ecosystem: Peteyvana, a B2B SaaS collaboration platform, tracked integration activation rate across its partner ecosystem. By analyzing usage data, the team discovered that while 60% of customers installed integrations, only 35% actively enabled and used them. After updating onboarding flows and surfacing high-value integrations in-app, activation rates rose to 68% within a quarter. Nearly doubling activation through better onboarding flows is not an outlier—it is what happens when you remove friction from the path to value.

A Before/After Measurement Framework

To quantify the impact of switching from static text inputs to dynamic dropdowns at your own company, capture baseline metrics before the change, then measure again at 30 and 90 days:

Metric What to Measure Expected Direction
Setup form completion rate % of users who load the config form and submit it successfully ↑ Fewer fields and recognizable options reduce abandonment
Time to first sync Minutes from OAuth completion to first successful data sync ↓ From days to hours when users can self-serve
Config support tickets Monthly tickets mentioning "setup," "ID," or "configuration" ↓ Users pick from a list instead of filing tickets
Integration activation rate % of connected accounts that reach "active" status ↑ See Peteyvana example: 35% to 68% in one quarter
Bounce rate on setup page % of users who land on the config screen and leave without interacting ↓ Dynamic options give users a reason to engage

The industry average SaaS activation rate sits at 36-37.5%, with a median of 30%. A rate above 40% is above average for complex B2B products, and above 50% is exceptional for multi-step setup environments. If your integration activation rate is below these benchmarks, post-connection configuration friction is a likely culprit. Even modest improvements pay off: the 5-7% lift per removed form field cited earlier means replacing three text inputs with a single dynamic dropdown can push you past the industry median.

Stop Losing Users to Bad Integration Onboarding

The gap between "connected" and "configured" is where integrations go to die. Every time a user abandons an integration setup flow because they do not know their "API Tenant ID" or subdomain name, you lose a chance to embed your product deeper into their daily workflows. Every blank text input asking for a raw ID is a drop-off point. Every manual step that could be automated is a support ticket waiting to happen.

Dynamic post-connection configuration is not just a UX nice-to-have; it is a critical requirement for scaling B2B SaaS. It fixes this by:

  • Eliminating user guesswork - show them their own data, don't ask them to type IDs or search external documentation.
  • Reducing error rates - constrain input to valid options fetched live from the API.
  • Accelerating time to value - fewer steps, less confusion, faster activation.
  • Scaling without per-integration code - declarative config means new integrations get dynamic forms automatically.

If your integration setup still asks users to manually enter workspace IDs, subdomain names, or record type identifiers, you are leaving activation (and revenue) on the table. The tooling exists to replace every one of those text fields with a live dropdown. The question is whether you build it yourself—one integration at a time—or use a declarative platform that handles it as configuration.

Legacy platforms force you to write custom JavaScript and manage complex pagination loops just to render a simple dropdown. By leveraging declarative architectures and tools like Truto RapidForm, you can automate the entire post-connection experience without writing a single line of integration-specific frontend code.

Stop letting bad onboarding kill your enterprise deals. Fix the connection flow, activate your users, and unblock your revenue.

FAQ

What is dynamic post-connection configuration?
It is the process of using freshly acquired API credentials to fetch account-specific data directly from a third-party API immediately after authentication, to populate user-facing setup forms with valid, selectable options rather than asking users to manually input IDs.
Why do users drop off during integration setup?
Users often abandon setup flows when confronted with confusing, manual configuration steps—like being asked to find and copy-paste internal API IDs, subdomains, or workspace tokens from external platforms.
How do dynamic dropdowns reduce integration onboarding drop-off?
Dynamic dropdowns constrain user input to valid, live options fetched from their own account, eliminating guesswork and typos. Each manual form field can reduce completion rates by 5-7%, so replacing text inputs with dropdowns directly improves activation.
How does Truto handle third-party API rate limits during setup?
Truto does not magically retry or apply backoff on rate limit errors. Instead, it passes the HTTP 429 error back to the caller and normalizes the rate limit data into standard headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) so your application can safely implement its own retry logic.
Do I need to write custom code for each integration's dynamic dropdown?
With traditional approaches like Zapier's developer platform, yes—you write custom JavaScript per integration. Declarative platforms like Truto let you define dynamic dropdowns as configuration with transform expressions, requiring no per-integration code.

More from our Blog