ACTION_ID: lookup_another_floqer_workflow_row NAME: Lookup Row from a Floqer Table CATEGORY: Data Operations CREDITS: 0 Look up a row in another Floqer table by matching a column value. Returns the matched row(s) plus a count of matches. 1. INPUTS table_to_search (string, required) — Target Table The destination sheet to search. Pass a `sheet_id` (UUID) for any sheet the caller's org owns — typically the master sheet that already holds the rows you want to find (e.g. "All Companies", "All Contacts"). Discovering the value: 1. GET /api/v1/workflows -> pick the workflow. 2. GET /api/v1/workflows/{workflow_id} (Get Workflow Overview) -> pick the sheet from `data.sheets[].sheet_id`. For the main sheet, `sheet_id` equals the workflow's own ID, so step 2 is optional — the workflow_id from step 1 works as a sheet_id directly. Example: "table_to_search": "7dca51c6-1308-4501-81a0-8d655c3bfd2a" target_column (string, required) — Target Column Identifier of the column on `table_to_search` to match on. This is a UUID — get it via Get Action Field Options: POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/ {action_instance_id}/options/target_column Body: { "context": { "target_table": "" } } The response is `{ options: [{ value, label }, ...] }` where each `value` is the column's id and `label` is the column's display name (e.g. "Email", "Domain"). Pass the chosen `value` straight into Configure Action — there's no separate lookup; the column id is what storage and the engine use. Note on naming: the options-call context key is `target_table` (NOT `table_to_search`). This is the only place where the two names diverge — everywhere else in the Configure Action body the field is `table_to_search`. Scope: input columns only. Action-output cells cannot be match keys. Get Action Field Options for `target_column` returns input columns only. If the match key lives in an action-output cell on the target sheet, either: (a) push the value into an input column on a destination sheet via `push_data_to_sheet` first, then look up against that sheet, or (b) match on any input column that identifies the same row (e.g. `linkedin_url`, `email`), and read the action output out of `record` downstream (see §2 and §6). operator (string, required) — Operator Match operator. Allowed values: "eq" exact match. "contains" target CELL contains `value` (value=needle, cell=haystack) — asymmetric: value "acme.com" matches cell "app.acme.com", not the reverse. Slower; false-positive prone. Use only when an exact key isn't available. Verified live 2026-07-24. value (string, required) — Target Value The value to search for. Can be a variable reference (e.g. `{{input.email}}`) or a static string. References resolve per-row at run time. find_all (boolean, optional) — Find All Rows Match one vs. match all. Default `false`. false returns the first match as a single object. true matches every row that satisfies the operator, returned as an array of objects. This flag also changes the shape of the `record` output — see section 2. 2. OUTPUTS record (object | array) — the matched row(s). Carries every cell on the matched row: input columns AND every action output. Asymmetric with `target_column` — matching is input-only; the returned `record` is the full row. Shape: - `find_all: false` (default): single object, or `{}` if no match. - `find_all: true`: array of objects, or `[]` if no match. Key naming inside each object: - EVERY column — input columns AND action-output cells alike → the column's DISPLAY name VERBATIM, including spaces, capitals, em-dashes, parentheses, `??`, etc. (e.g. `"Contact ID"`, `"Account ID"`, `"First Name"`, `"Email 1 — LLM-generated opener (Claude Sonnet 4.6)"`). No slug-ification, no snake_case, no rename — the key is exactly the column header shown in the UI. A column whose display name happens to be lowercase (`email`, `domain`) comes back lowercase and only LOOKS snake_cased — it is not being transformed. So read an input column by its real header: `record["Contact ID"]`, NOT `record["contact_id"]` (the latter resolves undefined). Verified live 2026-07-15. Multi-output actions: the value under the display-name key is the action's primary output (`result` / `generated_content` / the documented single value). Structured-array outputs are not exploded into per-field keys. Verified 2026-06-08. FILTER-ACTION CELLS ARE A TRAP. A filter action appears under its display name like any other, but its value is the filter's STATUS, not the value you filtered on: empty string `""` when the row passed, `conditionNotMet` when it was blocked. So `record["Filter Name"] === "high"` never matches — the cell never holds `"high"`. Verified 2026-06-08. See §6 for downstream access patterns. total (number) — total number of records that matched. `0` means no match. Always populated regardless of `find_all`. 3. HOW TO CONFIGURE Discovery flow (resolves the target sheet and column id before Configure Action): 1. Add the action. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/add Body: { "action_id": "lookup_another_floqer_workflow_row" } Returns the new `action_instance_id`. 2. Pick the target sheet via List Workflows → Get Workflow Overview (`data.sheets[].sheet_id`). GET /api/v1/workflows GET /api/v1/workflows/{target_workflow_id} Take the chosen sheet's `sheet_id`. For the workflow's main sheet, `sheet_id === workflow_id` so step 2 is optional. 3. Resolve `target_column` via Get Action Field Options. POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/ actions/{action_instance_id}/options/target_column Body: { "context": { "target_table": "" } } Pick the chosen option's `value` (a UUID) for the column you want to match on. The options call works without first PATCHing anything on the action — pass `target_table` inline in the context. 4. Configure the action with all five fields in a single PATCH. PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/ actions/{action_instance_id} Body: { "inputs": { "table_to_search": "", "target_column": "", "operator": "eq", "value": "{{input.email}}", "find_all": false } } Field-by-field: - table_to_search `sheet_id` (UUID) of the destination sheet. - target_column id (UUID) of the column to match on (from Get Action Field Options). - operator `"eq"` or `"contains"`. - value String literal or `{{...}}` reference. - find_all Boolean. `false` (default) returns one match; `true` returns all matches as an array. 4. KEY NOTES - Always branch on `total` before reading fields off `record` — `total === 0` means the lookup found nothing. - NO-MATCH ROWS DON'T RETURN A CLEAN `total: 0` — the action ends the cell as `failed` ("No matches") or, on some rows, `condition_not_met` — inconsistently, even across rows with the same config. This holds for both `find_all: false` and `find_all: true`. Two consequences: - A `condition_not_met` lookup HALTS the rest of that row's chain (like a filter) — every action downstream of the lookup is skipped for that row, so any extractor / push you place after it never runs on no-match rows. - `continue_workflow_if_action_fails: true` rescues the `failed` case (downstream still runs) but NOT the `condition_not_met` case (a run-condition skip, not a failure — see the run_if note in llms-full). So DON'T put must-run logic (e.g. an "in Salesforce? no" tag, or a push of the non-matching rows) downstream of the lookup. Put that work BEFORE the lookup, or drive it off a different signal, and treat the lookup + its `record` extractors as a MATCH-ONLY branch: matched rows (`complete`) extract `record` fine; no-match rows just stop there. Verified live 2026-07-15. - `record` shape depends on `find_all`: object when `false`, array of objects when `true`. Downstream actions that read individual fields work for the single-match case; for multi-match, use `formula` to iterate. - `target_column` is the column's id (UUID), NOT the column's snake_case name. Always pass the `value` returned by Get Action Field Options — typing a column name directly (e.g. `"email"`) will fail at run time. - RECORD KEYS ARE ACTION DISPLAY NAMES. Each cell on the matched row comes back keyed by the action's UI display name verbatim (spaces and caps included); the value is that action's PRIMARY output. To filter or extract an LLM sub-field that has no dedicated column of its own (it lives nested inside the action's output), add a `formula` column on the SOURCE sheet that surfaces it as its own cell — the lookup `record` then carries it by that formula's display name. See §2 and §6. Verified 2026-06-08. - FILTER CELLS HOLD STATUS, NOT THE FILTERED VALUE. A filter action's cell on `record` is keyed by its display name but its value is the filter status (`""` passed / `conditionNotMet` blocked), so `record["Filter Name"] === ""` never matches. To carry the underlying value, surface it with a source-sheet formula column (above). Verified 2026-06-08. - Get Options for `target_column` requires the target sheet id in the context, passed under the key `target_table` (NOT `table_to_search`). This is the one place the legacy and new names diverge: PATCH uses `table_to_search`; the options-call context uses `target_table`. Either pass it inline in the `context` body (recommended), or PATCH `table_to_search` first and then call options. - Free to run (0 credits). 5. WHERE IT FITS IN A WORKFLOW UPSTREAM — what feeds the lookup The current row carries the key (email, domain, etc.) you want to match against the destination table. The key reaches this action via {{input.}} — populated by an inbound trigger, webhook, list ingest, or an upstream enrichment step. THIS ACTION Searches `table_to_search` for rows where `target_column` matches `value` under `operator`. Returns `record` + `total`. DOWNSTREAM — typical chains Branch on found: this -> filter (total > 0) -> reuse data from `record` (enrichment, outreach, update). Branch on not found: this -> filter (total == 0) -> enrichment / create-row path. Cross-table consolidation: this -> push_data_to_sheet or google_sheet_upsert_row to merge the looked-up record into the current sheet. 6. ACCESSING RECORD FIELDS Matching is input-only (§1); `record` returns the full row as a json/object output (§2). Dotted nested reference syntax does NOT dive into `record` — `{{.record.}}` resolves to an empty string. The substitution layer only reaches top-level keys on an action's output schema; nested access requires bracket form. Canonical pattern — bracket access on `{{.record}}`: {{.record}}?.[""] This single form works for any key on the record. Keys are the column's DISPLAY name VERBATIM (§2) — for input columns too, NOT a snake_case slug: - Input-column keys: the column header exactly, e.g. `"Contact ID"`, `"Account ID"`, `"First Name"`, `"Email"`. A column literally named lowercase (`email`, `domain`) comes back lowercase; a column named `Contact ID` comes back `"Contact ID"`, NOT `contact_id`. - Action-output cells: the action's display name verbatim, including spaces, em-dashes, parentheses, `??` — e.g. `"Email 1 — LLM-generated opener (Claude Sonnet 4.6)"`. Examples (note the exact-header casing on input columns): {{lookup_another_floqer_workflow_row_01.record}}?.["Email"] {{lookup_another_floqer_workflow_row_01.record}}?.["Contact ID"] {{lookup_another_floqer_workflow_row_01.record}}?.["First Name"] {{lookup_another_floqer_workflow_row_01.record}}?.["Email 1 — LLM-generated opener (Claude Sonnet 4.6)"] This expression can be passed directly as an `input` value (pure-template form — no JS, no `return`, no IIFE), or used inside any downstream action input that accepts variable references. See https://floqer.com/docs/action-detail/formula.txt §1 (PURE FLOQER-EXPRESSION TEMPLATE). Anti-patterns: 1. Dotted nested access — `{{.record.}}`. Resolves to empty. Use bracket form instead. 2. JSON.parse the record blob: JSON.parse(`{{.record}}`)[""] The substituted blob is JSON-shaped but string-valued cells (multi-line dossiers / emails) keep their raw newlines. Strict `JSON.parse` rejects unescaped control characters inside string values; downstream `|| ""` masks the failure as empty. Use the canonical pattern. find_all: true — record is an ARRAY; `?.["key"]` reads one object only. To read across matches, bare-inject record into a formula and map it. Bare injection (NOT backtick- wrapped — see anti-pattern 2) emits a valid JS array literal with strings escaped, so multi-line cells are safe: (() => { const recs = {{.record}}; const arr = Array.isArray(recs) ? recs : (recs ? [recs] : []); return JSON.stringify(arr.map(r => ({ domain: r["Domain"] ?? "", account_id: r["Account ID"] ?? "" }))); })() Keys are display names (§2). Verified live 2026-07-24. Extracting one value per downstream variable (the standard pattern): When a downstream action needs ONE field as ONE variable (e.g. a `full_name` input on an email finder), the standard pattern is: upstream chain └─ lookup_another_floqer_workflow_row (returns `record`) └─ formula — extract one field formula: {{.record}}?.["Full Name"] (pure template — no return, no const, no IIFE) └─ formula — extract another field formula: {{.record}}?.["Company Website"] └─ inputs: full_name: {{.result}} company_domain: {{.result}} One formula per value you need as a separate variable downstream. Each formula's `result` is the clean single-value output the consuming action sees in its input. This avoids wrapping everything in one combined JSON blob that downstream actions then have to parse. Gating on no match: Always branch on `total` before reading from `record`. Pattern: next_action.run_if: {{.total}} greater than 0 7. WHEN TO USE Use lookup_another_floqer_workflow_row to reuse data that already lives in another Floqer table. Common patterns: - You run multiple use cases against the same list. Centralize the list in one master sheet, then look it up from each downstream workflow so enrichments aren't re-run. - You want to skip work that's already been done. Look the row up before paying for an enrichment or outreach step. - You need to read context from a master table onto a row in a campaign sheet. 8. WHEN NOT TO USE Need to send rows to another sheet or Floq, not read from one -> action-detail/push_data_to_sheet.txt -> action-detail/push_data_to_another_floqer_workflow.txt Last updated: 2026-07-15.