Floqer Public API — Complete Reference for AI Agents API endpoint base URL: https://api.floqer.com/api/v1 (for /api/v1/... endpoint paths in this file; all other URLs are absolute) Authentication: Bearer token. All requests require Authorization: Bearer floq_YOUR_API_KEY Rate limits: 200 requests/minute, 10,000 requests/day per API key. ================================================================================ FILE MAP — READ IN THIS ORDER ================================================================================ New here and about to BUILD or DEBUG a workflow? Read https://floqer.com/docs/docs.txt first — the short front door with the task reading order and the core build rules. This file is the deep endpoint reference you jump into for exact request/response shapes. https://floqer.com/docs/api/llms-full.txt — You are here. The master API reference. https://floqer.com/docs/llms-full.txt — DIFFERENT FILE. Every /docs product page inlined (guides and concepts for the app, not endpoint shapes). Not what you want if you are calling the API. https://floqer.com/docs/concepts.txt — How Floqer works. Mental model, vocabulary, data types. https://floqer.com/docs/action-catalog.txt — Find the right action for your data. https://floqer.com/docs/action-detail/{id}.txt — Full spec for a specific action. https://floqer.com/docs/source-catalog.txt — Find the right data source to import records into Floqer. https://floqer.com/docs/source-detail/{id}.txt — Full spec for a data source (preview/create body, filter fields, lifecycle). https://floqer.com/docs/use-case-catalog.txt — End-to-end workflow recipes indexed by GTM problem. https://floqer.com/docs/use-case-detail/{slug}.txt — Full recipe for one use case (chain of actions + narrative). https://floqer.com/docs/openapi.json — Machine-readable OpenAPI 3.0 spec. Use when you have OpenAPI tooling; otherwise stay in this file. https://floqer.com/docs/docs.txt — START HERE. Front-door orientation: the build-task reading order (know-the-customer → concepts → use-case-catalog → action/source details → llms-full) plus the core build rules. https://floqer.com/docs/reference — HTML page (not plain text). Human-readable single-page API reference with every endpoint, parameters, schemas, and curl examples. Open in a browser. ================================================================================ REQUIRED READING — https://floqer.com/docs/concepts.txt ================================================================================ Before using the endpoints below, fetch https://floqer.com/docs/concepts.txt. It is the single source of truth for Floqer's mental model and vocabulary: 1. Workflows and Sheets — container model, main sheet, sheet-scoped paths 2. Inputs — columns, types, reference strings 3. Actions — action_id vs action_instance_id, reference outputs 4. Data Types — scalars (string, url, email, number, boolean) for inputs and outputs; nested types (raw_array, json, structured_array) for action outputs only 5. Variable References — {{input.field}} and {{action_id.field}} syntax; structured_array column refs ({{action_id.list.column}}) 6. Action Chain — execution order, filter, push_data_to_sheet 7. Running a Workflow — add rows, run, check results, cache settings 8. Get Action Field Options — resolve dynamic dropdowns before configuring an action 9. Conditional Execution (run_if) — per-action row gating 10. Data Sources — list/preview/create/get data/sync/pause sources (CRM, list builders, monitors, pixels), selection + pull_mode lifecycle 11. Get Source Field Options — resolve dynamic dropdowns for source payloads 12. Caller Identity — GET /api/v1/user bootstrap (who the API key belongs to, org membership); org knowledge file (GET/PUT /api/v1/user/knowledge) 13. Shortcuts — run a published workflow as a typed function: input references, output names, single + batch runs, polling (formerly "Apps") 14. Web Agent (Floqer Nova) — one synchronous call that browses the web and returns structured JSON The endpoint reference below assumes this mental model. Do not skip it. ================================================================================ QUICK START — BUILD AND RUN A WORKFLOW ================================================================================ 0. Confirm caller identity (recommended first call) GET /api/v1/user → returns {email, first_name, last_name, role, org_id, org_members} No scope required. See concepts.txt §12. 1. Create a workflow POST /api/v1/workflows {"name": "Lead Enrichment Pipeline"} → returns workflow_id (this is also the main sheet's sheet_id) 2. Add inputs to the main sheet POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/inputs [{"name": "linkedin_url", "type": "url"}, {"name": "company_name", "type": "string"}] → returns inputs with reference strings: {{input.linkedin_url}} 3. Find the right action Read https://floqer.com/docs/action-catalog.txt — match your input types to action "Needs" 4. Add an action to the main sheet POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/actions/add {"action_id": "enrich_company_linkedin_profile"} → returns action_instance_id, inputs to configure, outputs with references 5. Configure the action PATCH /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/actions/{action_instance_id} {"inputs": {"linkedin_url": "{{input.linkedin_url}}"}} 6. Add data rows (start with 10–50 during build; use run_after_add="first_10" to verify safely) POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/rows {"rows": [{"linkedin_url": "https://linkedin.com/company/floqer", "company_name": "Floqer"}], "run_after_add": "first_10"} → returns {row_ids: [...], rows_queued_for_run: 1} 7. Run POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/run {"row_ids": ["returned-row-id"]} 8. Check results POST /api/v1/workflows/{workflow_id}/sheets/{workflow_id}/rows/list {"row_ids": ["returned-row-id"]} // or empty body to browse all rows paginated ================================================================================ OTHER API SURFACES — BEYOND BUILDING A WORKFLOW ================================================================================ The quick start above builds a sheet from scratch. Three other surfaces exist, each with its own section below. Pick the smallest one that does the job: Sources Pull records INTO Floqer from an external system (HubSpot, Salesforce, Sales Navigator, list builders, website pixels …), then sync them onto a workflow's rows. → ENDPOINTS — DATA SOURCES · concepts.txt §10–§11 Shortcuts Run an ALREADY-PUBLISHED workflow as a typed function: one POST with an input object, poll one run, read one output object. No sheets, no rows. Use this instead of building when the pipeline already exists. (Formerly "Apps" — /api/v1/apps/* still works.) → ENDPOINTS — SHORTCUTS · concepts.txt §13 Agent One SYNCHRONOUS call to Floqer Nova, the web agent: a prompt in, structured JSON out, no workflow attached and nothing to poll. → ENDPOINTS — AGENT · concepts.txt §14 ================================================================================ ENDPOINTS — BUILD A WORKFLOW ================================================================================ Inputs (sheet-scoped) --------------------- GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs — List Inputs Returns current input fields with reference strings. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs — Add Inputs Body: JSON array of {name, type, description?} Response: [{name, type, description, reference}] Declared `type` is metadata only — NOT enforced at row-ingest time. A field declared `"type": "number"` will accept the string `"Us"` (or any other value) without coercion or rejection on Add Rows / webhook ingest (see Add Rows note below) — the value is even stored back as a string (`5` round-trips as `"5"`). Type mismatches surface at downstream action runtime, not at ingest. Defensive pattern: coerce in a `formula` (`Number(String("{{...}}") || "0") || 0`). Verified 2026-06-08. DELETE /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs/{field_name} — Delete Input Removes an input. Breaks action references to {{input.field_name}}. Actions (sheet-scoped) ---------------------- POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/add — Add Action to Workflow Adds an action to the chain. Returns action_instance_id, inputs to configure, and outputs with reference strings. Body: {action_id, after?, name?, inputs?, run_if?, continue_workflow_if_action_fails?, note?} CONFIGURE IN THE SAME CALL. Every optional field Configure Action accepts — `inputs`, `run_if`, `continue_workflow_if_action_fails`, `note` — can be sent here too, with identical shapes and semantics (see Configure Action below for those shapes; they are not repeated here). This turns the usual add-then-configure pair into one request, which matters when building a chain action by action. Warnings come back on the 201 in the same `warnings[]` shape Configure Action returns. If configuration fails validation outright, nothing is created — there is no half-configured action left to clean up. `after` (optional) is the immediate predecessor at insertion time — the new action goes right after the named anchor and everything previously downstream of that anchor shifts down by one. To chain N actions sequentially in a single batch, set `after` of each call to the `action_instance_id` returned by the previous call. Repeated calls with the same `after` value (e.g. `after: "input"` N times) produce a chain in REVERSE of call order, because each new action is inserted immediately after the anchor and pushes the previously-inserted one further down. `name` (optional) overrides the action template's default display name. Whitespace-only values are ignored. The actual stored value is returned in `display_name` on the response. RESPONSE IS NOT THE CANONICAL INPUT LIST. The add response may OMIT some of the action's inputs — pre-filled or hidden ones the UI sets automatically. E.g. `hubspot_update_object` omits `hubspot_object_id` from the add response, but a subsequent GET Action returns it. Don't treat the add response's `inputs[]` as the full set; GET the action (or read its action-detail/{action_id}.txt) to see every input. Verified 2026-06-08. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id} — Configure Action Wires input variables to the action's fields. Body: {inputs?, run_if?, continue_workflow_if_action_fails?, note?} Waterfall (`stepDownSearch`) fields take an ordered array of provider DISPLAY NAMES — first is primary, the rest are fallbacks: {"inputs": {"email_providers": ["Hunter", "Prospeo", "Lightning"]}} To run a provider on YOUR OWN API key instead of Floqer's, pass an object for it instead of a bare name: {"inputs": {"email_providers": [ {"name": "Hunter", "use_own_key": true}, {"name": "Moltsets", "use_own_key": true}, "Prospeo" ]}} `use_own_key: true` bills the call to your account rather than Floqer credits, and requires that provider to be connected on the Connections page in the Floqer UI first — connections cannot be created over the API. Providers listed as plain names keep whatever key state they already had, so reordering never silently moves you back onto Floqer credits. Configure Action reports waterfall problems as non-fatal `warnings[]` on a 200: `unknown_field` (unknown or duplicated provider, or an array that resolved to nothing — the stored waterfall is then left untouched), `ambiguous_provider` (token matched one provider's name and another's legacy id; resolved by name), `unsupported_own_key` (`use_own_key` on a provider with no BYOK mode; ignored), `own_key_required` (a provider with no Floqer-billed mode configured without `use_own_key`). Get Action returns the field in the same mixed shape: providers on your own key as `{"name": ..., "use_own_key": true}`, the rest as plain names, so the value round-trips. Reorder Waterfall Providers accepts that mixed array too — bare names or `{"name": ...}` — and ignores `use_own_key` (warning `own_key_ignored` when it disagrees with stored state), since it only reorders. Get Action Field Options reports which providers support this (`extras.supports_own_key`) and which ONLY run on your own key (`extras.requires_own_key`). Provider ids are never accepted as the documented form nor returned — always use display names. `inputs` values must be strings, string arrays, object arrays, or booleans. Send every numeric field as a QUOTED STRING — `"60"`, not `60`. A raw JSON number is dropped with a non-fatal `warnings[]` entry (`code: "unknown_field"`) and the field stays at whatever it was before, so the action silently runs with the old value. This applies to every `number`, `integer`, `currency` and `slider` field in the catalog (e.g. `rate_limiter.requests`, `salesloft_upsert_person.owner_id`, `gohighlevel_create_opportunity.monetary_value`, `delay_step.time`). Booleans are the exception — `radio` / `boolean` fields take real `true` / `false`. `run_if` (optional) gates whether this action runs for a row. It is an array of AND/OR condition GROUPS — the same shape the `filter` action uses for `path_conditions`: run_if: { conditions: [ { conditions: [ { variable: "{{input.country}}", operator: "is", values: ["US"] }, { variable: "{{input.tier}}", operator: "is", values: ["A"], combinator: "AND" } ] }, { conditions: [ { variable: "{{input.name}}", operator: "starts with", values: ["a"] } ], combinator: "OR" } ], continue_workflow_if_run_condition_not_met: false } • Outer array = groups, joined by each group's `combinator` (AND/OR). Each group's `conditions` = leaf conditions, joined by each leaf's `combinator`. The first group's / first leaf's `combinator` is ignored; omitted defaults to AND. • A single condition is just one group with one leaf. • `operator` is the comparison (`is`, `greater than`, ...); `combinator` is the AND/OR join between conditions. Operator support depends on the variable's stored type — see concepts.txt §9 for the full list. • `continue_workflow_if_run_condition_not_met` lives INSIDE `run_if` (default false), NOT at the body's top level — don't confuse it with the top-level `continue_workflow_if_action_fails`. Verified 2026-06-08. • Pass `run_if: null` to clear an existing condition. `run_if: {}` is ACCEPTED as a no-op (leaves any existing condition untouched) — it is NOT rejected; use `null`, not `{}`, when you actually want to clear. Verified 2026-06-08. • Legacy: the old flat single-condition form `{variable, operator, values}` is still ACCEPTED on write for backward compatibility (NOT rejected) — it is auto-normalized to the nested form — but is no longer documented; use the nested `conditions` form. Writing it returns a non-fatal `warnings[]` entry (`code: "deprecated_format"`) asking you to migrate. Get Action and Get Action Graph ALWAYS return the condition in the nested form, whichever way it was written. Verified 2026-06-08. `continue_workflow_if_action_fails` (optional, top-level; defaults true) controls what happens to the REST of the chain when THIS action fails on a row. It has a clear, observable effect — don't treat it as a no-op: • `true` (default) — downstream actions still run even though this action's cell is `failed`/`error`. Use when the failing step is optional enrichment and later steps don't strictly need its output. • `false` — a failure here HALTS the chain for that row; downstream actions never run and their cells never appear in `cells{}`. Either way the row's `row_status` becomes the sticky `has_failures` (the failed cell is what trips it) — the flag changes downstream EXECUTION, not the row-level failure flag. It is a top-level body field; do NOT confuse it with `continue_workflow_if_run_condition_not_met`, which lives inside `run_if` and governs the run_if gate, not failures. Verified 2026-06-08. `note` (optional) saves a free-text note on the action — same upsert as Save Action Note. Whitespace-only values are ignored. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/name — Rename Action Sets an action instance's display name. Single-field PATCH — the only body field is `name` (required, trimmed, non-empty). Body: {name: string} Renaming does NOT invalidate the row-level cache for this action — display name doesn't change what the action does. Only the workflow's `updated_at` timestamp is bumped. Reserved `action_instance_id` values (`"input"`, `"graph"`) return 404. Response: {status, data: {action_instance_id, display_name}}. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/move — Move Action Re-orders an action within the sheet's chain. Updates the chain pointers (`nextAction` / `prevAction`) on the moved action, on its old neighbours (relinked so the chain skips it at the source position), and on its new neighbours (relinked so the chain points through it at the destination position). The action's configured inputs / outputs are not touched. Body: {after: string} -- required • "input" → move to the start (right after the synthetic input node) • → move immediately after that instance `after` cannot equal the action being moved. Cannot move `id1` (the input node — chain anchor); reserved values `"input"` / `"graph"` return 404. Reference safety: Move does NOT rewrite configured inputs. If the new position pushes the action ahead of one of its upstream references — or pushes a downstream consumer ahead of this action — those refs will fail at run time. Fix afterwards via Configure Action. Response: {status, data: {moved: true, action_instance_id, after}}. `after` in the response is the canonical public form — `"input"` when the action is now at the start of the chain. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/waterfall/{field_name} — Reorder Waterfall Providers Re-orders the provider list on a waterfall (`stepDownSearch`) input field. Pass the snake-cased `field_name` from Add Action's `inputs[]` (e.g. `email_providers`) and a `providers` array listing every currently configured provider BY DISPLAY NAME exactly once, in the desired execution order. Body: {providers: ["Hunter", "Prospeo", "Lightning"]} Names are matched case-insensitively. Discover the valid names with Get Action Field Options on the waterfall field, or read the current set from Get Action's `inputs[]` — both return display names. Reorder only — does not add or remove providers. To change which providers are enabled, or to run one on your own API key, use Configure Action. Bring-your-own-key state set on a provider is preserved across a reorder. Same cache invalidation as Configure Action (this action and every downstream action on the sheet). Response: {status, data: {action_instance_id, field_name, providers}} — `providers` echoes display names in their new order. DELETE /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id} — Delete Action Removes an action from the sheet's chain by relinking its previous and next neighbours so the chain skips it. The deleted action's configured inputs / outputs are gone. Cannot delete the input node — pass any other `action_instance_id`. Reserved values (`"input"`, `"graph"`) return 404. No request body. Response: {status, data: {deleted: true, action_instance_id, dependent_actions}}. `dependent_actions[]` lists every action whose configuration referenced one of the deleted action's outputs. Each entry carries `action_instance_id` plus an `inputs` object containing ONLY the affected field keys (snake-cased); values are the stored references translated to public reference form so the broken refs are obvious. Unrelated fields on the same dependent action are not included. Empty array when nothing depended on the deleted action. Reference safety: any field surfaced in `dependent_actions[]` now contains an `unresolved_reference` to the deleted action. Fix each via Configure Action before the next row run, otherwise those rows will fail. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/notes — Save Action Note Saves a free-text note on an action instance. Upsert against the composite primary key `(action_instance_id, sheet_id)` on `public_api_notes` — calling again for the same action overwrites the existing note. Notes are surfaced on Get Action and on each node of Get Action Graph as `note` (absent when no note has been saved). Body: {note: string} -- required, trimmed, non-empty Response: {status, data: {action_instance_id, sheet_id, note}}. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/graph — Get Action Graph Returns every node in the sheet (input node + actions) in topological execution order, each with its {{reference}} strings and next-edge topology. Each node also carries `note` when one has been saved on the action via Save Action Note or Configure Action. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id} — Get Action Returns a single action's node shape. Same schema as one node of Get Action Graph (so any saved note is included as `note`). Round-trips with Configure Action on the same URL. Includes validation warnings for broken references. Readback caveat for structured-output actions (`llm_models`, `llm_web_agents`): the `output_format` field in this response is a type DISCRIMINATOR string — `"fields"` for the structured-fields form, `"json"` for raw JSON — NOT the schema object you sent in Configure Action. The persisted schema is not echoed back through Get Action. To verify what fields the action will emit, look at `outputs[]` (or call Get Action Outputs on the same instance). POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/options/{field_name} — Get Action Field Options Also resolves the provider list on a waterfall (`stepDownSearch`) field: returns each selectable provider as `{value, label}` display names, with `extras.supports_own_key` / `extras.requires_own_key` flagging which can (or must) run on your own API key. Disabled providers are omitted. Resolves a dynamic dropdown for the named field. Required before configuring any action that has a dynamicDropdown / spreadsheetsDropdown / hubspot_properties_dropdown / externalIdDropdown / inSheetDropdown / etc. Body: {"context"?: {"": ""}} Some fields depend on other selections — e.g. hubspot_object_properties needs {"context": {"hubspot_object": "contacts"}}. Pass {} when no context is required. Response: {"options": [{value, label}, ...]}. Pass the chosen `value` directly into the matching field in Configure Action. See per-action HOW TO CONFIGURE in https://floqer.com/docs/action-detail/{action_id}.txt for the exact pre-call(s) each action needs. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/outputs — Get Action Outputs Returns the resolved output schema for an action — every field it produces, each with a ready-to-paste `reference` token for downstream wiring. No request body. Static actions (Salesforce / HubSpot / etc.) carry their schema from the moment they're added — calling this on a static action is fine but not necessary; the same `outputs[]` is on Get Action / Get Action Graph. One exception: `hubspot_lookup_object`'s `records` columns are chosen per action instance via `output_columns`, so they are known only once the action has been configured, not at add time. Dynamic-output actions (`http_api_call`, `raw_to_structured_array`) only know their output shape after a row runs. The discovery sequence is: 1. Configure the action with realistic inputs (Configure Action). 2. Add ONE example row whose data exercises the action, then Run Rows on just that row. The action's worker stores the upstream response under `action.ping`. 3. Call Get Action Outputs. The handler reads the captured `ping`, derives the field/column schema, mints stable `responseId`s, persists the new schema onto the action, and returns it. 4. ⚠ The example row from step 2 ran BEFORE the schema existed — its cell in List Rows will not surface the discovered outputs (the runtime had no response to associate values with). To get a row whose cell carries the typed outputs, either re-run the example row (Run Rows again) or delete it (Delete Rows) and run fresh rows. From this point on every new run sees the persisted schema. See https://floqer.com/docs/action-detail/http_api_call.txt and https://floqer.com/docs/action-detail/raw_to_structured_array.txt for the per-action walkthrough. Sheets (workflow-scoped) ------------------------ POST /api/v1/workflows/{workflow_id}/sheets — Create Sheet Body: {name, auto_run?, cache_enabled?, cache_since?} Returns: full sheet object {sheet_id, workflow_id, name, auto_run, cache_enabled, cache_since} GET /api/v1/workflows/{workflow_id} — Get Workflow Overview Bootstrap call after List Workflows. Returns workflow metadata (name, timestamps, last_run_at), sharing info (is_owner, shared_users), and a per-sheet roster with settings (auto_run, cache_*) and webhook URLs for pushing rows. data.sheets[0] is always the main sheet (sheet_id === workflow_id, is_main_sheet: true). Child sheets follow sheets_config.sheetsOrder when set, otherwise creation order. shared_users is populated only when is_owner is true; other callers get []. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/name — Rename Sheet Scope: workflows:write. Sets a sheet's name. Single-field PATCH — the only body field is `name`. Body: {name: string} -- required, trimmed, non-empty Response: {status: 200, data: {workflow_id, sheet_id, name}}. `name` is echoed back after trimming, so no follow-up Get Workflow Overview is needed to confirm what was stored. Works on the main sheet too — pass sheet_id === workflow_id. Renaming the main sheet renames its TAB only; the workflow keeps its own name. Get Workflow Overview then returns the new label as data.sheets[0].name while data.name is unchanged. To rename the workflow itself, use PATCH /api/v1/workflows/{workflow_id}/name (Rename Workflow). The two names are stored separately. Renaming touches only the name — rows, cached action runs, and the action chain are unaffected. Sheet names are not required to be unique within a workflow. Any caller with access to the workflow may rename a sheet; unlike Delete Sheet this is NOT restricted to the sheet owner. Whitespace-only `name` → 400 "name is required and cannot be empty". Unknown sheet_id, or a sheet_id belonging to a different workflow → 404 "Sheet not found". Status codes: 200 / 400 (empty name) / 401 / 403 / 404 (sheet not found) / 429. DELETE /api/v1/workflows/{workflow_id}/sheets/{sheet_id} — Delete Sheet Scope: workflows:write. Permanently deletes a child sheet and every row on it. Archives the sheet and its action chain; row data and run history on the sheet are no longer reachable. Cannot be undone. No request body. Pass the child sheet's UUID as sheet_id — not the parent workflow ID. Response: {status: 200, data: {workflow_id, sheet_id, deleted: true}}. Only the sheet owner can delete a child sheet. Shared users and org members with workflow access → 403 "Only the sheet owner can delete this sheet." Main sheet (sheet_id === workflow_id) cannot be deleted → 400 "Cannot delete the main sheet. Delete the parent workflow instead, or pass a child sheet_id." To remove the main sheet, delete the parent workflow via DELETE /api/v1/workflows/{workflow_id}. Unknown / already-deleted sheet_id → 404 "Sheet not found". Status codes: 200 / 400 (main sheet) / 401 / 403 (not sheet owner) / 404 (sheet not found) / 429. ================================================================================ ENDPOINTS — RUN A WORKFLOW ================================================================================ POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows — Add Rows Body: {rows: [{: , ...}], run_after_add?: "none" | "first_10" | "all"} Response: {row_count, row_ids: [...], rejected: [{row: {...original payload...}, errors: [...]}], rows_queued_for_run} No type validation — values written as-is; type issues surface at action runtime. Missing fields → null. Unknown fields → row echoed in rejected with error details. Partial success, never atomic. row_ids pipes directly into Run Rows's row_ids. run_after_add is scoped to THIS call's rows only — pre-existing rows on the sheet are not touched. Max 1,000 rows per call. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run — Run Rows Body: {row_ids: ["id1", "id2"]} -- specific rows OR {first_10: true} -- shortcut: first 10 rows on the sheet (by created_at asc) Exactly one of row_ids or first_10 required. Both or neither → 400. Execution is async — returns immediately. Response: {status, message, data: {rows_queued}}. Rows run CONCURRENTLY, not one-after-another. Queuing N rows runs them in parallel — wall-clock time does NOT scale linearly with row count. Anchor any "how long will this take" estimate on the length of a SINGLE row's action chain, not on row_count. Verified 2026-06-08. STALE-CHAIN GOTCHA — re-running an already-run row after adding a NEW downstream action can report `row_status: "complete"` PREMATURELY without ever running the new action. An action added since the row last ran has no cell on that row; on re-run it may queue separately and NOT be counted toward `row_status` — the row flips to `complete` with the new action's cell absent from `cells{}` entirely and never executed. This is intermittent (sometimes the re-run does reach the appended cell), so do not rely on Run Rows to pick up freshly-added downstream actions. Reliable fix: call Run Action with `run_next_action: true` on the FIRST newly-added action — that mints the missing cell, runs it plus everything downstream, and settles `row_status` correctly. Verified 2026-06-08. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run-all — Run All Rows No body required. Queues every row on the sheet. ⚠️ Full credit cost = row_count × actions. Never call without first verifying on a sample. Response: {status, message, data: {rows_queued}}. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/run — Run Action Runs a specific action against a set of rows. By default ONLY the targeted action runs — downstream actions are not touched. Set `run_next_action: true` to continue down the chain after this action finishes, re-running every action wired off its outputs. Body: {row_ids: [...], run_next_action?: boolean} `row_ids` is required. Pass `[]` to queue every row on the sheet (analogous to Run All Rows but scoped to one action). Pass explicit IDs (from Add Rows / List Rows) to target a subset. `run_next_action` `false` (default) → just this action; `true` → this action plus everything downstream. Useful after Configure Action when you want to refresh a single action and everything wired off its outputs in one call. Reserved `action_instance_id` values (`"input"`, `"graph"`) return 404. Async — returns immediately. Response: {status, message, data: {rows_queued}}. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/list — List Rows POST (not GET) — request body carries filter + pagination so large row_id arrays don't overflow the URL. Body (all optional): { row_ids?: [...] (max 200), page_no?: 1, page_size?: 20 (max 200), filters?: [...], // input-column value filters status_filters?: [...], // per-action cell-status filters created_at?: { operator, values } // row creation time filter } Omit row_ids to browse all rows on the sheet (paginated). Include row_ids to fetch just those. row_ids cannot combine with filters / status_filters / created_at — picking explicit row IDs and filtering at the same time is rejected with 400. Browse-mode filtering: `filters[]` — value filters on **sheet input columns** only (action-output filtering not exposed yet). Each entry: { "variable": "{{input.}}", "operator": "", "values": ["v1", ...] } Operators (matches the frontend's filter-bar dialect): is equal to (1 value) not equal to (1 value) is empty (0 values) not empty (0 values) contains (1 value, substring) does not contain (1 value, substring) contains any of (1+ values, exact match each) not contains any of (1+ values, exact match each) greater than (1 value, numeric) less than (1 value, numeric) `status_filters[]` — match rows by per-action cell status. Each entry: { "action_instance_id", "is" | "is_not": [...] }. Exactly one of `is` / `is_not`. Status values are the public vocabulary (queued | running | complete | failed | error | condition_not_met). `queued` matches any cell waiting to run. `created_at` — single time filter on row creation. Shape: { "operator": "", "values": [""] } Operators (separate dialect from filters[]): equals to (1 ISO timestamp) greater than (1 ISO timestamp) greater than or equals to (1 ISO timestamp) less than (1 ISO timestamp) less than or equals to (1 ISO timestamp) is between (2 ISO timestamps: [start, end]) Pattern — filter rows by action OUTPUT content (the filter-as-tag idiom) `filters[]` is sheet-inputs-only. To filter rows by an action output value, put a `filter` action in the chain that gates on the upstream output, then query that filter action's status via `status_filters[]`: Workflow setup: ... -> upstream action producing field X -> filter (`{{.X}}` is/is_not ) -> ... downstream Query body: {"status_filters": [ {"action_instance_id": "", "is": ["complete"]} ]} `is: ["complete"]` returns rows where the gate passed. `is: ["condition_not_met"]` returns the inverse — rows the gate rejected. The filter action's cell-status mirrors the condition outcome (see action-detail/filter.txt §4 KEY NOTES). Multi-condition / non-trivial-predicate variant — JS sentinel + filter When the gate is a predicate that doesn't fit `filter`'s operators (nested OR/AND over several action outputs, arithmetic combined with substring tests, type-coerced comparisons), wrap the predicate in a `formula` returning "YES" / "NO" and put a filter on `is "YES"` after it: ... -> formula (returns "YES" or "NO") -> filter (`{{.result}}` is "YES") -> ... downstream Query the filter's status the same way. This is the canonical way to make any computable predicate over action outputs queryable later via rows/list. Pagination — pages past `total_count` return `rows: []` with the actual sheet size in `total_count`. Detect end-of-pagination by `rows.length === 0`, not by checking `total_count` against your page math (which would be ambiguous when filters shrink the result set). Response envelope (full shape): { "status": 200, "data": { "rows": [...], // RowView[], one per row on this page "total_count": , // total rows being paginated — sheet-wide // count in browse mode, or the size of the // `row_ids` filter you passed "page_no": , // echoes back the requested page (default 1) "page_size": // echoes back the requested page size (default 20) } } Each row (RowView): {row_id, row_status, created_at (ISO 8601 UTC), inputs, cells}. row_status: "pending" (never run) | "running" (any cell queued/running) | "complete" (all cells complete) | "has_failures" (terminal, at least one cell failed or errored). `has_failures` is STICKY — once any cell fails on a row, the row-level flag stays even if every other cell completes cleanly afterwards (and even if the failing cell later passes on a re-run). Use per-cell `cells[].status` to verify whether downstream data is actually present; don't rely on `row_status` alone for "is this row usable." Verified 2026-06-08. cells: object keyed by `action_instance_id` (the public form, exactly the same ID Add Action / Get Action returned). Empty {} for rows that have never been run. Each cell: {status: "queued"|"running"|"complete"|"failed"|"error"|"condition_not_met", outputs?: {...}, outputs_ref?: {url, size_bytes}, error?: string}. `failed` and `error` are both terminal failure states — `failed` is a known failure path (integration 404, validation rejected); `error` is an unexpected runtime error. Both carry an `error` message; treat them the same when polling for completion. `outputs` keys are the action's snake-cased public output names — same keys you'd see in the Add Action `outputs[].name` and the `{{ref}}` token after the dot. `outputs` (inline) and `outputs_ref` (large-payload URL — fetch with API key) are mutually exclusive. `structured_array` outputs (e.g. `list_of_employees`) come back as a flat array of objects keyed by the column's snake_case name — `[{first_name: "Satya", last_name: "Nadella", ...}, ...]`. Empty / missing values for an output may surface as `""` rather than the typed empty (e.g. `[]` for arrays, `null` for booleans). Treat all outputs as nullable on the consumer side. Poll this after runRows; branch on row_status for the quick check, cells[action_id].status for per-cell detail. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete — Delete Rows Body: {row_ids: ["uuid1", "uuid2", ...]} (max 200). Response: {deleted_count, deleted_row_ids: [...], rejected: [{row_id, error}]}. Partial success — UUIDs not found on this sheet come back in rejected. Cannot be undone. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete-all — Delete All Rows Body: send an empty JSON object `{}`. No fields in the body are used. The empty-body rejection is CONDITIONAL on the Content-Type header: when you send `Content-Type: application/json` with an empty body the framework returns 400 `FST_ERR_CTP_EMPTY_JSON_BODY` ("Body cannot be empty when content-type is set to 'application/json'"). (Omitting the Content-Type header entirely returns 200 with no body — but most HTTP clients set it automatically when you send JSON, so always pass `{}` to satisfy the validator.) Verified 2026-06-08. Deletes every row on the sheet. Cannot be undone. Response: {deleted_count}. ================================================================================ ENDPOINTS — MANAGE WORKFLOWS ================================================================================ POST /api/v1/workflows — Create Workflow Body: {name} Returns: {workflow_id, name, created_at} — workflow_id is also the main sheet's sheet_id. GET /api/v1/workflows — List Workflows Scope: workflows:read. Returns all workflows the caller can access as summaries: [{workflow_id, name, created_at, updated_at, last_run_at}]. last_run_at is null for workflows that have never been run. Status codes: 200 / 401 / 403 / 429. GET /api/v1/workflows/{workflow_id} — Get Workflow Overview Scope: workflows:read. Bootstrap call after List Workflows — returns workflow metadata, sharing info, and a per-sheet roster with settings and webhook URLs. Response: {status: 200, data: { workflow_id, name, created_at, updated_at, last_run_at, is_owner, -- true when caller owns the workflow shared_users: [...], -- populated only when is_owner; else [] sheets: [{ sheet_id, name, is_main_sheet, -- sheets[0] is always the main sheet auto_run, cache_enabled, cache_since, webhook_url -- POST URL for webhook row ingest; null if unset }, ...] }}. Child sheets follow sheets_config.sheetsOrder when set, otherwise creation order. Use data.sheets[].sheet_id when configuring send_to_sheet, lookup_another_floqer_workflow_row, or any sheet-scoped path. There is no GET .../sheets list endpoint — sheet IDs live here. Status codes: 200 / 401 / 403 / 404 / 429. PATCH /api/v1/workflows/{workflow_id}/name — Rename Workflow Scope: workflows:write. Sets the workflow's name. Single-field PATCH — the only body field is `name`. Body: {name: string} -- required, trimmed, non-empty Response: {status: 200, data: {workflow_id, name}}. Does NOT rename the main sheet's tab. A workflow's name and its main sheet's label are stored separately; this endpoint writes the workflow's. Get Workflow Overview reports it as data.name. data.sheets[0].name mirrors it only while the main sheet has no label of its own — once you rename the main sheet via Rename Sheet, the two move independently. Renaming touches only the name — sheets, rows, cached action runs, and the action chain are unaffected. Any caller with access to the workflow may rename it; unlike Delete Workflow this is NOT restricted to the owner. Whitespace-only `name` → 400 "name is required and cannot be empty". Status codes: 200 / 400 (empty name) / 401 / 403 / 404 / 429. DELETE /api/v1/workflows/{workflow_id} — Delete Workflow Deletes the workflow and all its sheets, inputs, actions, rows, and run history. Cannot be undone. Returns: {workflow_id, deleted: true}. ================================================================================ ENDPOINTS — USER ================================================================================ GET /api/v1/user — Get User Info Returns the API-key owner's profile plus a summary of their organization and its members. No request body, no query params. Response: {status: 200, data: { email, first_name, last_name, -- API-key owner role, -- e.g. "admin" org_id, -- organization UUID org_members: { count, -- total member count emails: [...] -- raw invite list; may contain -- duplicates if the same address was -- invited more than once (no dedup -- at invite time). `count` equals -- `emails.length`, not the distinct -- count. } }}. GET /api/v1/user/knowledge — Get Org Knowledge Reads the organization's knowledge file: a single free-form text/markdown blob describing the org (who they are, what workflows they run, their goals). One file per org. Read this for context before acting. Response: {status: 200, data: { content, -- the file text, or null if never written exists, -- boolean; false when no file has been written yet updated_at -- ISO 8601 timestamp of last write, or null }}. Note: when unset this returns exists:false / content:null with 200 — it is NOT a 404. No request body, no query params. PUT /api/v1/user/knowledge — Set Org Knowledge Creates or replaces the organization's knowledge file. Idempotent full replacement — `content` becomes the entire file. Send an empty string to clear it. Body: { content: string } -- max 256 KB (UTF-8) Response: {status: 200, data: { content, exists: true, updated_at }}. ================================================================================ ENDPOINTS — DATA SOURCES ================================================================================ Sources pull external data (e.g. from HubSpot) into Floqer, where it becomes available to your workflows for enrichment, scoring, outreach, and the like. See https://floqer.com/docs/concepts.txt §10 for the mental model and https://floqer.com/docs/source-detail/{source_id}.txt for per-source body shapes, filter operator catalogues, and configuration walkthroughs. GET /api/v1/sources — List Sources Scope: sources:read. Lists the sources you've created, newest first. Only source types this API supports are listed. Response: {status: 200, data: [{source_instance_id, name, source_id, status, lead_count, created_at, expiration_date}, ...]}. `source_instance_id` is the source INSTANCE UUID — pass it to `POST /api/v1/sources/{source_instance_id}/sync` or `GET /api/v1/sources/{source_instance_id}/data`. `source_id` is the source-type slug (e.g. `import_from_hubspot`) used on the preview / create / options endpoints. `status` is one of: `active` (live, re-importing), `completed` (one-time import, won't refresh), `paused`, `paused_out_of_credits`, `expired`, `deleted`. `lead_count` and `expiration_date` may be null. Status codes: 200 / 401 / 403 / 429. Three parametric operations serve every source TYPE. `{source_id}` is the source type slug (e.g. `import_from_hubspot`). Three instance-scoped operations apply to a created source — Sync, Get Source Data, and Pause / Resume — keyed by `{source_instance_id}`, the UUID returned by Create. Preview / Create bodies are per-source — the route schema only enforces that the body is a JSON object; per-source validation runs server-side and returns 400 with a specific message. Read source-detail/{source_id}.txt for the exact body. POST /api/v1/sources/{source_id}/preview — Preview Source Scope: sources:read. Returns up to 100 records that WOULD be imported for the given body, without creating anything. Use this to validate filters / selections before committing to Create. Body: the per-source preview payload, plus one source-agnostic key: `limit` (optional, positive integer) caps how many rows come back. Useful when you only need to see the shape of a row rather than the whole sample. It is applied by this endpoint, so it works on every source type; it does NOT reduce what the source fetches upstream and does NOT change `metadata.total_results`. When it drops rows, `metadata.capped` is true. Response: {status: 200, data: {data: [{...}], metadata: {total_results, capped?}}}. `data[]` is capped at 100 records. `metadata.total_results` is the total match count (best-effort; may lag writes by a few seconds) — it reports the upstream count, not the number of rows returned. Status codes: 200 / 400 (per-source validation) / 404 (unknown source_id) / 424 (integration not connected) / 401 / 403 / 429. POST /api/v1/sources/{source_id} — Create Source Scope: sources:write. Creates the source, imports matching records immediately (when the source's body opts into it, e.g. `pull_existing`), and — for an ongoing source — keeps re-importing on a schedule. Response: {status: 201, data: {source_instance_id: "", name, created_at}}. `source_instance_id` is the new source's UUID — distinct from the `{source_id}` type slug in the URL (which names the source TYPE). Status codes: 201 / 400 (per-source validation) / 402 (credit consumption failed — the source is created but its import can't start; top up and retry) / 404 (unknown source_id) / 424 (integration not connected) / 401 / 403 / 429. Async behavior: the call returns as soon as the source is created and the import is queued. Records arrive over seconds/minutes depending on import size — not by the time the call returns. POST /api/v1/sources/{source_instance_id}/sync — Sync Source to Workflow Scope: sources:write. Connects a CREATED source to a workflow so its records flow into that workflow, and (by default) backfills the workflow with the source's existing rows. Here `{source_instance_id}` is the source INSTANCE UUID returned by Create. Body: { workflow_id: "" -- required field_mapping: { "input.": "", ... } -- required, non-empty push_existing: boolean -- optional, defaults true run: "none" | "first_10" | "all" -- optional, defaults "all" } `field_mapping` keys are public workflow input references (`input.`, same form as `{{input.}}` without braces); each is resolved to that workflow input. Each value is the source field to pull — the row's top-level key exactly as it appears in Preview (e.g. `email`, `Name`, `website`), never a nested path like `Name.value`. Cells that render as `{label, value}` are unwrapped automatically. Keys that don't match a workflow input → 400. `run` (only relevant when push_existing): "all" runs the workflow on every backfilled row, "first_10" runs the first 10 and just loads the rest, "none" loads rows without running. Building field_mapping (two lookups): 1. GET /api/v1/workflows (List Workflows) → pick the destination workflow_id. 2. GET /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs (List Inputs) → each input has a `reference` like `{{input.email}}`. 3. Per input you want to fill: key = that reference WITHOUT braces (`input.email`), value = the source field (from Preview rows, or the hubspot_properties options call). e.g. {"input.email": "email", "input.first_name": "firstname"} Keys are CASE-SENSITIVE — copy the reference verbatim (lowercase snake_case); `Input.Email` / `input.firstName` return 400. Response: {status: 201, data: {source_instance_id, workflow_id, push_existing, run, fields_mapped}}. Status codes: 201 / 400 (bad body or unknown field_mapping input) / 403 (no access to the workflow) / 404 (source not found / not owned) / 409 (source already connected to this workflow — update the existing connection instead of re-syncing) / 401 / 429. GET /api/v1/sources/{source_instance_id}/data — Get Source Data Scope: sources:read. Returns paginated rows imported into a created source. `{source_instance_id}` is the source INSTANCE UUID returned by Create. Query params: `page_no` (1-indexed, default 1), `page_size` (default 20, max 200). Response: {status: 200, data: {rows: [{...}], total_count, page_no, page_size}}. Row field names match Preview for that source type. Use after Create to poll while import runs, or to page the full dataset before Sync. When `page_no * page_size > total_count`, `rows` is []. Status codes: 200 / 400 (invalid pagination) / 404 (source not found / not owned) / 401 / 403 / 429. PATCH /api/v1/sources/{source_instance_id}/status — Pause or Resume Source Scope: sources:write. Pauses or resumes a CREATED source. `{source_instance_id}` is the source INSTANCE UUID returned by Create. Body: {status: "active" | "paused"} `paused` stops recurring runs (schedule, webhooks where applicable); `active` resumes them. Static / one-time sources have nothing recurring to pause, but the status still updates. Response: {status: 200, data: {source_instance_id, status}}. Status codes: 200 / 400 / 404 / 401 / 403 / 429. POST /api/v1/sources/{source_id}/options/{field_name} — Get Source Field Options Scope: sources:read. Resolves dynamic option values for a source-payload field (behaves like the workflow Get Action Field Options endpoint). Body: {"context"?: {"": ""}}. Response: {status: 200, data: {options: [{value, label, extras?}, ...]}}. Pipe the chosen `value` straight into the matching field in the source's preview / create body. Status codes: 400 — that field has no options for this source, OR a required context key is missing. 404 — unknown source_id. 424 — integration not connected. 502 — integration call failed. ──────────────────────────────────────────────────────────────────────── DISCOVERING SOURCES + PER-SOURCE BODIES ──────────────────────────────────────────────────────────────────────── The operations above are identical for every source. What differs is the per-source preview/create BODY (the filters / fields you send). 1. Browse https://floqer.com/docs/source-catalog.txt for the full list of source types grouped by category — each entry shows the source_id, its category, whether it needs a connection, and its key body fields. 2. Read https://floqer.com/docs/source-detail/{source_id}.txt for the exact body shape, the full filter-field catalogue with allowed values, the dynamic-options fields, and an end-to-end walkthrough. ALWAYS read this before constructing a create body — filter operators and enums are source-specific. Bodies are snake_case and reject unknown top-level keys. Sources that take a `filters` object reject unknown filter keys too — resolve allowed values via the source's options endpoint (Get Source Field Options above) or the source-detail filter catalogue; never guess. ================================================================================ ENDPOINTS — SHORTCUTS (RUN A WORKFLOW AS A TYPED FUNCTION) ================================================================================ A shortcut wraps a published workflow behind a typed input/output schema so it can be run as a single call — pass one input object, poll one run, read one output object. Shortcuts are created and published in the Floqer UI; these endpoints CONSUME them. See https://floqer.com/docs/concepts.txt §13 for the mental model. Scopes: shortcuts:read (list, get, read runs), shortcuts:run (start runs). The flow: List Shortcuts → Get Shortcut (read input_schema for the reference keys to send, output_schema for the name keys to expect back) → Run Shortcut (or Run Shortcut Batch for up to 1,000 rows) → poll Get Shortcut Run (or List Shortcut Runs with batch_id) until a terminal status → read output_data. LEGACY /api/v1/apps/* — STILL SUPPORTED Shortcuts shipped originally as "Apps". The old URL prefix and the old `apps:read` / `apps:run` scope names keep working permanently: a request to `/api/v1/apps/...` is folded onto `/api/v1/shortcuts/...` before routing, and the old scope names remain valid aliases. Existing integrations need no change; new code should use `/shortcuts`. ⚠ The wire format did NOT rename with the URL — a run object still carries `app_id` (not `shortcut_id`), and Get Shortcut still returns `parent_app_id` for shared copies. That is the field name, not a typo. In the product UI these are called Shortcuts; older docs and the MCP tool names (list_apps / get_app / run_app / list_app_runs) still say "app". GET /api/v1/shortcuts — List Shortcuts Scope: shortcuts:read. Returns every shortcut the caller can access — ones they created (master) and ones shared with them (shared). Each entry carries the shortcut's FULL input_schema and output_schema, so a single List call is enough to discover what is runnable and what each shortcut takes and returns; you do not need a follow-up Get Shortcut unless you want to re-read one. Query params: `filter` = master (shortcuts you created) | shared (shared with you) | all (default). Response: {status: 200, data: [{id, workflow_id, parent_app_id, name, description, creator_email, org_id, input_schema, output_schema, is_master, is_published, is_archived, cover_image, icon_emoji, created_at, updated_at}, ...]}. Only `is_published: true` shortcuts can be run. Status codes: 200 / 401 / 403 / 429. GET /api/v1/shortcuts/{id} — Get Shortcut Scope: shortcuts:read. The bootstrap call before running one. Returns full details: input_schema: [{reference, name, description?, type, required?, defaultValue?}] — `reference` is the snake_case field name (e.g. `linkedin_url`) derived from `name`; use it as the key in `input_data` when running. Check `required` and `defaultValue` to know what you must send. output_schema: [{name, description?, display_column?}] — `name` (e.g. `Company Website`) is the key under which the field's value appears in a run's `output_data`. is_published: only published shortcuts can be run; running an unpublished one returns 403. No request body. Response: {status: 200, data: {...shortcut}}. Status codes: 200 / 400 (invalid UUID) / 404 / 401 / 403 / 429. POST /api/v1/shortcuts/{id}/run — Run Shortcut Scope: shortcuts:run. Starts one run with the provided input. Execution is ASYNC — the call returns 201 immediately; poll Get Shortcut Run for status and results. Body: {"input_data": {"": "", ...}} Keys must be `reference` values from the shortcut's input_schema. Unknown keys → 400 listing the valid references. Missing fields fall back to the shortcut's input `defaultValue`s, then to "". Response: {status: 201, message: "Shortcut run started", data: {id, data_id, status: "pending", created_at}}. `id` is the run ID — poll GET .../runs/{id}. `data_id` is the row created in the underlying workflow for this run. Status codes: 201 / 400 (unknown input fields) / 403 (shortcut unpublished, or missing scope) / 404 / 401 / 429. POST /api/v1/shortcuts/{id}/run/batch — Run Shortcut Batch Scope: shortcuts:run. Starts one run per input row, grouped under a single batch_id. Max 1,000 rows per request. Validation is ALL-OR-NOTHING — one unknown key in any row rejects the whole batch (the 400 names the offending row index); no partial batches are created. Body: {"rows": [{"": "", ...}, ...]} Response: {status: 201, message: "Shortcut batch run started", data: {batch_id, run_count}}. Follow the batch via GET .../runs?batch_id={batch_id} until every run reaches a terminal status. Status codes: 201 / 400 (empty rows, >1,000 rows, unknown input fields) / 403 (shortcut unpublished, or missing scope) / 404 / 401 / 429. GET /api/v1/shortcuts/{id}/runs — List Shortcut Runs Scope: shortcuts:read. Returns a shortcut's runs, newest first. Query params: `limit` (default 50, max 100), `offset` (default 0), `batch_id` (narrow to the runs created by one Run Shortcut Batch call), `mode` = single (runs from Run Shortcut) | batch. Response: {status: 200, data: {runs: [...run objects...], pagination: {limit, offset, total}}}. ⚠ For FILTERED queries (batch_id or mode) `total` is the returned page's row count, NOT the full filtered count — paginate until a page returns fewer rows than `limit`. Unfiltered queries report the true total. Status codes: 200 / 400 (invalid UUID) / 404 / 401 / 403 / 429. GET /api/v1/shortcuts/{id}/runs/{run_id} — Get Shortcut Run Scope: shortcuts:read. The polling target after Run Shortcut. Returns the run with live status — for in-progress runs the status is recomputed from the workflow engine, so this reflects real-time progress. Response: {status: 200, data: {id, app_id, workflow_id, data_id, user_email, org_id, input_data, output_data, status, error_message, created_at, completed_at}}. `status` terminal values: completed | failed | error | outOfCredits. In-progress values include pending and inProgress. Poll until terminal. `input_data` echoes back reference-keyed values. `output_data` is null until the run finishes, then an object keyed by each output field's `name` from output_schema, e.g. {"Name": {"value": "Avish Jain", "status": "completed"}, "Email": {"value": "avish@floqer.com", "status": "completed"}} Each entry carries its own per-field `status` — a run can complete with some fields failed ("No data found"). Check per-field status before trusting a value; do not assume `status: "completed"` at the run level means every field resolved. `app_id` is the shortcut's id (legacy field name — see the note above). Status codes: 200 / 400 (invalid UUIDs) / 404 (shortcut, run, or a run not belonging to this shortcut) / 401 / 403 / 429. Shortcuts vs workflows — when to use which Use a SHORTCUT when the workflow already exists and you just need its result for one record (or a batch): one POST, one poll, typed in/out. Use the WORKFLOW endpoints when you need to build or modify a pipeline, run large ongoing datasets on sheets, or inspect per-action cells. ================================================================================ ENDPOINTS — AGENT (FLOQER NOVA) ================================================================================ Floqer Nova is the web agent, exposed as a single SYNCHRONOUS call. Give it a natural-language prompt and, optionally, an output_schema describing the JSON fields you want back; it browses the web and returns the answer in the same response. See https://floqer.com/docs/concepts.txt §14. Unlike every other run endpoint in this file, this one is NOT asynchronous — there is nothing to poll. POST /api/v1/agent/run — Run Agent Scope: agent:run. Body: { "prompt": "...", -- required, 1–20,000 chars "output_schema": { -- optional "properties": { "": {"type": "string", "description": "..."} } } } `output_schema` is a JSON-schema-style object. Supported field types: string, number, integer, boolean, array, object. Max 20 fields. When omitted, the full answer comes back under a single `result` field. Response: {status: 200, data: {...}, citations: [...], request_id: "fl_..."}. `data` is keyed by your output_schema fields, or {result} when no schema was given. `citations` lists the web sources the agent consulted. `request_id` (e.g. `fl_…`) is on EVERY response, success or failure — quote it in support requests. Timing: requests typically take 10–60 seconds and the connection stays open for the whole run. The server gives up at 150 seconds and answers 504 AGENT_TIMEOUT — set your client timeout ABOVE that, or you will time out locally on runs that would have succeeded. Billing: one Floqer Nova charge per request. Any 5xx is refunded automatically — a 5xx means you were NOT charged. Rate limiting: agent calls have their own org-level quota, separate from the general 200/min · 10,000/day API limits. Error codes (in `error` / `message`): 400 INVALID_REQUEST, INVALID_OUTPUT_SCHEMA 401 UNAUTHORIZED 402 INSUFFICIENT_CREDITS 403 INSUFFICIENT_SCOPE -- key lacks agent:run 429 RATE_LIMITED 500 AGENT_INTERNAL_ERROR, AGENT_EXECUTION_FAILED -- not charged 503 AGENT_BUSY -- not charged; retry after `Retry-After` 504 AGENT_TIMEOUT -- not charged Example: POST /api/v1/agent/run { "prompt": "Find the current CEO and headcount of Floqer.", "output_schema": { "properties": { "ceo_name": {"type": "string", "description": "Full name"}, "headcount": {"type": "integer", "description": "Current employees"} } } } → {"status": 200, "data": {"ceo_name": "...", "headcount": 42}, "citations": [...], "request_id": "fl_..."} Agent vs the llm_web_agents action: this endpoint is a one-shot call with no workflow attached. To run a web agent across every row of a sheet, add the `llm_web_agents` action to a workflow instead — see https://floqer.com/docs/action-detail/llm_web_agents.txt. ================================================================================ UI LINKS — PRESENTING WORKFLOWS TO THE USER ================================================================================ When directing a user to view a workflow in the UI, construct a full link with view + sheet — the bare `/workflow/{id}` redirects to the build view of the main sheet, which is usually not specific enough. URL template: https://app.floqer.com/workflow/{workflow_id}?action={view}&v2=true&sheetId={sheet_id} Path and query parameters: workflow_id UUID. Required. action=build Node-graph view. UI label "Floq". Surface after chain edits or when the user is auditing pipeline shape. action=table Spreadsheet view. UI label "Data". Surface after row runs or when the user is reviewing per-row outputs. v2=true Required UI version flag. sheetId UUID. Required. Equal to workflow_id for the main sheet; a different UUID for sub-sheets created via POST /workflows/{id}/sheets. Always include — the bare link redirects to the main sheet. UI label ≠ URL param. The UI labels these views "Floq" (build) and "Data" (table). When writing to the user, refer to them by their UI labels — "here's the Floq view of the chain", "results are in the Data view" — not by the URL params, which the user isn't seeing. Defaults: - Reporting chain edits → Floq link - Reporting row results → Data link - Touching a sub-sheet → link directly to that sub-sheet's Data view (sheetId = sub-sheet UUID), not the parent main sheet Examples: Floq (chain): https://app.floqer.com/workflow/?action=build&v2=true&sheetId= Data (rows): https://app.floqer.com/workflow/?action=table&v2=true&sheetId= Sub-sheet rows: https://app.floqer.com/workflow/?action=table&v2=true&sheetId= When linking both views in one response, label clearly: - Floq (build view): - Data (table view): ================================================================================ ERRORS ================================================================================ Success: {"status": 200, "data": {...}} Single errors (401, 403, 404, 429, 500): {"status": 401, "error": "Unauthorized", "message": "API key is required"} Validation errors (400) — all field errors at once: {"status": 400, "error": "Validation Error", "message": "2 validation errors", "errors": [{"field": "name", "message": "required"}]} 429 includes retryAfter: {"status": 429, "error": "Too Many Requests", "message": "...", "retryAfter": 42} ================================================================================ This file is maintained manually. Last updated: 2026-09-01. Full interactive reference: https://floqer.com/docs/reference