Dynamiq
Deploy & Integrate

End-User Connection Requirements

Let each end user of a deployed App link their own account or credentials — define requirements at build time, detect unmet ones at run time, and fulfill them via the hosted connect page or the connect API.

When a workflow node needs a Connection — or an Action tool needs a linked account — you have two choices at build time: authorize it yourself, so every run of the deployed App uses your credentials, or flag it as a requirement, so every end user must connect their own account before the App runs for them. This page covers the full requirement lifecycle: defining requirements on a workflow, discovering unmet requirements for a user, and getting them fulfilled through the hosted connect page or your own UI.

How requirements work

A requirement is a named placeholder attached to a workflow. Instead of pointing a node at a concrete Connection or account, you point it at the requirement. The deployed App then resolves the placeholder per end user at run time, using the connection that user linked for that App.

There are two requirement types:

TypeWhat the end user providesSpec
connectionCredentials (or an OAuth grant) for one of Dynamiq's connection types — an API key, OAuth consent, etc.{"type": "dynamiq.connections.OpenAI"} — the connection type the node needs
pipedream_accountAn action connector account — the linked third-party account (Gmail, Slack, …) that an Action tool acts through{"app_slug": "gmail"} — the app the Action targets

The lifecycle is always the same:

  1. Build time — the builder creates a requirement on the workflow and selects it on the node instead of a concrete Connection or account.
  2. Deploy — the App is deployed; the requirement travels with the workflow version.
  3. Discover — before running the App for a user, your backend asks the App whether that user_id has satisfied all requirements.
  4. Fulfill — for users with pending requirements, you mint a short-lived connect token and either send them to the hosted connect page or drive the connect API from your own UI.
  5. Run — you call the App with the same user_id; the platform resolves each flagged node to that user's linked connection or account.

The user_id is any stable string your application uses to identify the end user — the same field you send with runs for conversations and sessions. Requirements are tracked per App per user_id.

Shared connection or per-user requirement?

ChooseWhen
Connection / account selected at build timeThe credential belongs to your organization: your LLM provider key, your internal database, a service account. Every end user's runs share it, and end users never see a setup step.
RequirementThe node must act as the end user: reading their inbox, posting to their workspace, querying their tenant. Each user authorizes once per App; credentials are stored as system-managed, end-user-scoped connections that other users and runs can't touch.

A single workflow can mix both — for example, a shared connection for the LLM node and a per-user requirement for the email Action the agent calls.

Define requirements at build time

Requirements belong to a workflow, so save the workflow first — the Requirements tab is disabled on an unsaved workflow.

Connection-backed nodes

Open the connection chooser

Select the node on the canvas and click its Connection field in the configuration panel. A segmented control appears with two tabs: Connections (concrete connections, shared by all users) and Requirements.

Node configuration panel with the Connections / Requirements segmented control and the Requirements tab selected

Create the requirement

On the Requirements tab, click + New requirement. The Add New Requirement sheet asks for:

  • Connection Type — limited to the types this node supports; locked after creation.
  • Name — internal identifier, e.g. openai; locked after creation.
  • Form Title — what end users see on the connect page, e.g. OpenAI API Key (1–128 characters).
  • Description (optional) — helper text under the title (up to 256 characters).

Click Create. The new requirement is selected on the node automatically.

Add New Requirement side sheet with Connection Type, Name, Form Title, and Description fields

Verify the selection

The node's Connection field now shows the requirement name with a (Requirement) suffix instead of a connection name. Multiple nodes can reference the same requirement. Deleting a requirement clears it from every node that references it.

Action tools

For an Action node, the account field has its own Accounts | Requirements segmented control. Accounts lets the builder link an account on the spot (shared by all end users); Requirements lists this app's requirements with the same + New requirement flow — the Name and Form Title are prefilled from the app (for example Gmail Account), and the app slug is set automatically from the Action's configuration.

API equivalent

Requirements have a CRUD API on the management API (https://api.getdynamiq.ai, authenticated with a Personal Access Token):

Method & pathBodyNotes
GET /v1/workflows/{workflow_id}/requirementsPaginated list
POST /v1/workflows/{workflow_id}/requirements{name, type, form, spec}type is connection or pipedream_account
GET /v1/workflows/{workflow_id}/requirements/{requirement_id}Single requirement
PUT /v1/workflows/{workflow_id}/requirements/{requirement_id}{form}Only the form (title, description) is editable
DELETE /v1/workflows/{workflow_id}/requirements/{requirement_id}Returns {"message": "deleted"}
curl -X POST "https://api.getdynamiq.ai/v1/workflows/$WORKFLOW_ID/requirements" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DYNAMIQ_PAT" \
  -d '{
    "name": "openai",
    "type": "connection",
    "form": {
      "title": "OpenAI API Key",
      "description": "Used to run the assistant on your own OpenAI account."
    },
    "spec": {
      "type": "dynamiq.connections.OpenAI"
    }
  }'
curl -X POST "https://api.getdynamiq.ai/v1/workflows/$WORKFLOW_ID/requirements" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DYNAMIQ_PAT" \
  -d '{
    "name": "gmail",
    "type": "pipedream_account",
    "form": {
      "title": "Gmail Account",
      "description": "The agent sends email replies from this account."
    },
    "spec": {
      "app_slug": "gmail"
    }
  }'

Both return the created requirement under data:

{
  "data": {
    "id": "7b3f1c2a-9d4e-4f6b-8a1c-2e5d7f9b0c3d",
    "type": "connection",
    "form": {
      "title": "OpenAI API Key",
      "description": "Used to run the assistant on your own OpenAI account."
    },
    "spec": {
      "type": "dynamiq.connections.OpenAI"
    },
    "workflow_id": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
    "name": "openai"
  }
}

Creating a requirement via the API does not wire it to a node — set the node's connection (or the Action's account) to a requirement reference in the workflow definition, which is what the UI does when you select it.

Discover unmet requirements

Before running the App for a user, ask the App itself. Both endpoints live on the App hostname and — like any call to a private App — take an Access Key when Endpoint Authorization is enabled. Only requirements actually referenced by a node in the deployed workflow version are reported.

  • GET https://<your-app-hostname>/v1/requirements — every requirement the deployed version declares.
  • GET https://<your-app-hostname>/v1/requirements/status?user_id=<id> — the same list filtered to what this user still has to do.

The status response is either complete:

{ "status": "completed" }

or lists what is missing:

{
  "status": "incomplete",
  "unsatisfied": [
    {
      "id": "7b3f1c2a-9d4e-4f6b-8a1c-2e5d7f9b0c3d",
      "type": "connection",
      "form": {
        "title": "OpenAI API Key",
        "description": "Used to run the assistant on your own OpenAI account."
      },
      "spec": {
        "type": "connection",
        "data": { "type": "dynamiq.connections.OpenAI" }
      },
      "workflow_id": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
      "name": "openai"
    }
  ]
}

A connection requirement counts as satisfied only while the user's linked connection is active — an OAuth flow the user started but never finished stays unsatisfied.

Connect tokens and the hosted connect page

To let a user fulfill requirements, mint a connect token — a bearer token scoped to one App and one user_id, valid for 24 hours:

curl -X POST "https://<your-app-hostname>/v1/connect/tokens" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
  -d '{"user_id": "user-42"}'
{
  "token": "eyJhbGciOi...",
  "url": "https://<dynamiq-ui-host>/connect?token=eyJhbGciOi...",
  "expires_at": "2026-06-12T09:30:00Z"
}

The url points to the hosted Setup Requirements page. Send the user there (link, email, or redirect) and they see:

  • A Pending section listing each unmet requirement by its form title and description, with a Connect button.
  • A Completed section for requirements they have already fulfilled.

For a connection requirement, Connect opens a side sheet with the credential form for that connection type (for example an API key field); submitting it stores the credentials and marks the requirement completed. For an action connector account requirement, Connect opens the account authorization page in a new tab; once the user finishes linking the account, the requirement flips to completed.

Hosted Setup Requirements page showing pending requirements with Connect buttons and a completed section

screenshot: deployments-connect-page-pending

Connect tokens expire after 24 hours and are scoped to a single user and App. Mint a fresh token each time you send a user to the page — don't store or reuse them across users.

Fulfill requirements from your own UI

If you'd rather keep users inside your product, the same connect token authorizes the connect API on the management API (https://api.getdynamiq.ai). All five endpoints take the connect token as the Bearer token — no Access Key or Personal Access Token:

Method & pathBodyReturns
GET /v1/connect/requirementsAll requirements for the token's App
GET /v1/connect/requirements/statusSame list with a per-item status: pending or completed
POST /v1/connect/requirements/{requirement_id}/credentials{type, config}{"message": "created"} — submit credentials for a non-OAuth connection requirement
POST /v1/connect/requirements/{requirement_id}/oauth2/authorize{"data": {"url": "..."}} — provider consent URL for an OAuth connection requirement
POST /v1/connect/requirements/{requirement_id}/pipedream/authorize{"data": {"url": "..."}} — hosted authorization URL for an action connector account requirement

A status item looks like this — spec.data.type tells you which credential form to render, spec.data.app_slug which account to link:

{
  "data": [
    {
      "id": "7b3f1c2a-9d4e-4f6b-8a1c-2e5d7f9b0c3d",
      "type": "connection",
      "title": "OpenAI API Key",
      "description": "Used to run the assistant on your own OpenAI account.",
      "status": "pending",
      "spec": {
        "type": "connection",
        "data": { "type": "dynamiq.connections.OpenAI" }
      }
    },
    {
      "id": "1f2e3d4c-5b6a-4789-9a0b-1c2d3e4f5a6b",
      "type": "pipedream_account",
      "title": "Gmail Account",
      "status": "pending",
      "spec": {
        "type": "pipedream_account",
        "data": { "app_slug": "gmail" }
      }
    }
  ]
}

Rules the API enforces:

  • The type in a credentials submission must exactly match the requirement's connection type, or you get a connection type mismatch error.
  • OAuth connection types (Google, Microsoft, Notion, and the other OAuth connections) are rejected by the credentials endpoint — use oauth2/authorize and redirect the user to the returned URL instead. The connection stays incomplete (and the requirement pending) until the user finishes the provider's consent screen.
  • pipedream/authorize only works on pipedream_account requirements, and credentials/oauth2/authorize only on connection requirements.

Connections created this way are system-managed and end-user-scoped: they never appear in the project's Connections list and are only used for this user's runs of this App.

There is also a server-side shortcut that skips connect tokens entirely: POST https://<your-app-hostname>/v1/connections with {"user_id", "requirement_id", "type", "config"} (Access Key auth) creates and activates an end-user connection directly. Use it when your backend already holds the user's credentials — never expose it to browsers.

The full round trip

Check status, mint a token, fulfill what's pending, then run the App. Replace the hostname, ids, and connection types with your own.

# 1. Is user-42 ready to run the app?
curl "https://<your-app-hostname>/v1/requirements/status?user_id=user-42" \
  -H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY"

# 2. Not yet ("status": "incomplete") - mint a 24h connect token
curl -X POST "https://<your-app-hostname>/v1/connect/tokens" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
  -d '{"user_id": "user-42"}'
# -> {"token": "<CONNECT_TOKEN>", "url": "...", "expires_at": "..."}

# 3. List what is pending, with specs
curl "https://api.getdynamiq.ai/v1/connect/requirements/status" \
  -H "Authorization: Bearer $CONNECT_TOKEN"

# 4a. Fulfill a connection requirement with credentials
curl -X POST "https://api.getdynamiq.ai/v1/connect/requirements/$REQUIREMENT_ID/credentials" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $CONNECT_TOKEN" \
  -d '{
    "type": "dynamiq.connections.OpenAI",
    "config": {"api_key": "<the end user'\''s key>"}
  }'

# 4b. Or get an authorization URL for an action connector account requirement
curl -X POST "https://api.getdynamiq.ai/v1/connect/requirements/$REQUIREMENT_ID/pipedream/authorize" \
  -H "Authorization: Bearer $CONNECT_TOKEN"
# -> {"data": {"url": "https://..."}}  open this URL for the user

# 5. Re-check, then run the app with the same user_id
curl "https://<your-app-hostname>/v1/requirements/status?user_id=user-42" \
  -H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY"
import os
import requests

app_endpoint = "https://<your-app-hostname>"
api_base = "https://api.getdynamiq.ai"
end_user_id = "user-42"

app_headers = {"Authorization": f"Bearer {os.getenv('DYNAMIQ_ACCESS_KEY')}"}

# 1. Is the end user ready to run the app?
status = requests.get(
    f"{app_endpoint}/v1/requirements/status",
    params={"user_id": end_user_id},
    headers=app_headers,
).json()

if status["status"] == "completed":
    print("All requirements satisfied - run the app.")
else:
    print("Unsatisfied:", [r["form"]["title"] for r in status["unsatisfied"]])

    # 2. Mint a connect token (valid for 24 hours)
    token = requests.post(
        f"{app_endpoint}/v1/connect/tokens",
        json={"user_id": end_user_id},
        headers=app_headers,
    ).json()

    # Option A: hand off to the hosted connect page
    print("Hosted setup page:", token["url"])

    # Option B: fulfill programmatically with the connect token
    connect_headers = {"Authorization": f"Bearer {token['token']}"}
    requirements = requests.get(
        f"{api_base}/v1/connect/requirements/status",
        headers=connect_headers,
    ).json()["data"]

    for req in requirements:
        if req["status"] == "completed":
            continue

        if req["type"] == "connection":
            conn_type = req["spec"]["data"]["type"]
            if conn_type == "dynamiq.connections.OpenAI":
                # Non-OAuth connection: submit the user's credentials directly
                requests.post(
                    f"{api_base}/v1/connect/requirements/{req['id']}/credentials",
                    json={"type": conn_type, "config": {"api_key": os.getenv("END_USER_OPENAI_KEY")}},
                    headers=connect_headers,
                ).raise_for_status()
            else:
                # OAuth connection: redirect the user to the consent URL
                auth = requests.post(
                    f"{api_base}/v1/connect/requirements/{req['id']}/oauth2/authorize",
                    headers=connect_headers,
                ).json()
                print("Send the user to:", auth["data"]["url"])

        elif req["type"] == "pipedream_account":
            # Action connector account: open the hosted authorization URL
            auth = requests.post(
                f"{api_base}/v1/connect/requirements/{req['id']}/pipedream/authorize",
                headers=connect_headers,
            ).json()
            print("Send the user to:", auth["data"]["url"])
const appEndpoint = "https://<your-app-hostname>";
const apiBase = "https://api.getdynamiq.ai";
const endUserId = "user-42";

const appHeaders = {
  "Content-Type": "application/json",
  Authorization: `Bearer ${process.env.DYNAMIQ_ACCESS_KEY}`,
};

interface RequirementStatus {
  id: string;
  type: "connection" | "pipedream_account";
  title: string;
  status: "pending" | "completed";
  spec: { type: string; data: { type?: string; app_slug?: string } };
}

async function ensureRequirements() {
  // 1. Is the end user ready to run the app?
  const statusRes = await fetch(
    `${appEndpoint}/v1/requirements/status?user_id=${encodeURIComponent(endUserId)}`,
    { headers: appHeaders },
  );
  const status = await statusRes.json();

  if (status.status === "completed") {
    console.log("All requirements satisfied - run the app.");
    return;
  }

  // 2. Mint a connect token (valid for 24 hours)
  const tokenRes = await fetch(`${appEndpoint}/v1/connect/tokens`, {
    method: "POST",
    headers: appHeaders,
    body: JSON.stringify({ user_id: endUserId }),
  });
  const { token, url } = await tokenRes.json();

  // Option A: hand off to the hosted connect page
  console.log("Hosted setup page:", url);

  // Option B: fulfill programmatically with the connect token
  const connectHeaders = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  };

  const reqsRes = await fetch(`${apiBase}/v1/connect/requirements/status`, {
    headers: connectHeaders,
  });
  const requirements: RequirementStatus[] = (await reqsRes.json()).data;

  for (const req of requirements) {
    if (req.status === "completed") continue;

    if (req.type === "connection" && req.spec.data.type === "dynamiq.connections.OpenAI") {
      // Non-OAuth connection: submit the user's credentials directly
      await fetch(`${apiBase}/v1/connect/requirements/${req.id}/credentials`, {
        method: "POST",
        headers: connectHeaders,
        body: JSON.stringify({
          type: req.spec.data.type,
          config: { api_key: process.env.END_USER_OPENAI_KEY },
        }),
      });
    } else if (req.type === "connection") {
      // OAuth connection: redirect the user to the consent URL
      const authRes = await fetch(
        `${apiBase}/v1/connect/requirements/${req.id}/oauth2/authorize`,
        { method: "POST", headers: connectHeaders },
      );
      console.log("Send the user to:", (await authRes.json()).data.url);
    } else {
      // Action connector account: open the hosted authorization URL
      const authRes = await fetch(
        `${apiBase}/v1/connect/requirements/${req.id}/pipedream/authorize`,
        { method: "POST", headers: connectHeaders },
      );
      console.log("Send the user to:", (await authRes.json()).data.url);
    }
  }
}

ensureRequirements();

Once the status check returns "completed", call the App with the same user_id — the platform resolves every requirement-flagged node to that user's linked connection or account for the run.

Scoping reference

Three credential systems coexist on the platform — don't mix them up:

MechanismScopeWhere it's set up
Connection selected on a node at build timeShared — every run, every end userWorkflow canvas / Connections
Workflow requirement (connection or pipedream_account)One per end user per App — always per-user, never sharedHosted connect page or connect API
Chat ConnectorsPer platform user in ChatChat settings — a separate system from App requirements

Troubleshooting

Next steps

On this page