ACTION_ID: ackdb_ingest NAME: ackDB: Ingest event CATEGORY: Integration CREDITS: 0 Send a workflow row into ackDB as an event — resolving (or creating) the company and the person it belongs to. ================================================================================ PREREQUISITE — USER ACTION REQUIRED ================================================================================ This action only works if the user has connected their ackDB tenant in Floqer. The connection cannot be made by an AI agent. The user must do this themselves before this action will run: 1. Sign in to Floqer at https://app.floqer.com 2. Open the Connections page from the sidebar 3. Click ackDB and enter the tenant Base URL and API key (Base URL is per-tenant, e.g. https://acme.api.ackdb.ai) If the user has not connected ackDB, this action fails with "No ackDB connection found for the user". Before configuring or running this action, confirm with the user that the ackDB connection exists. INDEX: 1. Inputs 2. Outputs 3. How to configure 4. Key notes 5. Where it fits in a workflow 6. When to use 7. When not to use ================================================================================ 1. INPUTS ================================================================================ source (dropdown, required) Source pack name registered in ackDB (e.g. email, slack, stripe, hubspot, usage, web). Determines the metadata schema ackDB validates the event against. Options are fetched live from the org's ackDB connection, so the list reflects that tenant's registered packs. body (ackdbMapper, required) The full ackDB ingest body as JSON. Use {{vars}} to interpolate workflow row values. `source` is merged in automatically — do not set it here. Shape: content.text (string, REQUIRED) human-readable summary of the event. Omitting it is an error. content.metadata (object, REQUIRED) the event's fields. Validated against the source pack's schema — required fields must be present and correctly typed. identityLinks (array, REQUIRED in practice) how the event resolves to a company. Each entry: { externalIdType, externalIdValue, confidence }. `domain` is the universal create-key. traits (array, optional) trait writes: [{ traitKey, value }]. timestamp (string, optional) ISO-8601. Defaults to now. entityId (string, optional) pin to a known company, skipping domain resolution. contactId (string, optional) PERSON RAIL — pin the event's actor to this exact contact. See KEY NOTES. personId (string, optional) PERSON RAIL — pin to this exact person. See KEY NOTES. ================================================================================ 2. OUTPUTS ================================================================================ entityId (string) — Entity ID. The company the event resolved to (created if it did not exist). contentId (string) — Content ID. The stored event's id. created (boolean) — Entity Created. True when this event minted the company. deduplicated (boolean) — Deduplicated. True when ackDB recognised this event as one it already had and did not write a duplicate. identityLinksCreated (number) — Identity Links Created. traitsWritten (number) — Traits Written. ================================================================================ 3. HOW TO CONFIGURE ================================================================================ Resolve the source list via Get Action Field Options before configuring: POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/options/sourceId (body: {}) `body` is passed as a JSON STRING. Shown here unescaped for readability — escape it (or use the UI's Body builder, which does that for you): { "content": { "text": "Sent onboarding email to {{input.email}}", "metadata": { "event_type": "email.sent", "timestamp": "{{input.sent_at}}", "from_email": "team@acme.com", "message_id": "{{input.message_id}}", "subject": "{{input.subject}}", "is_internal": false, "primary_recipient_email": "{{input.email}}" } }, "identityLinks": [ { "externalIdType": "domain", "externalIdValue": "{{input.domain}}", "confidence": 1 } ], "traits": [] } Configure Action body (the same body, escaped into the `body` string): { "inputs": { "source": "email", "body": "{\"content\":{\"text\":\"Sent onboarding email to {{input.email}}\",\"metadata\":{ ... }}, \"identityLinks\":[ ... ],\"traits\":[]}" } } To PIN the event to a contact resolved upstream, add the rail to the same body — safe to wire unconditionally (see KEY NOTES): "contactId": "{{lookup.contactId}}" Field-by-field: - source Pack name from the `sourceId` options call. Picks the schema the metadata is validated against. - body The ingest envelope as a JSON string. Every {{var}} is substituted before the POST. The UI also offers a structured Body builder (a form over the source pack's schema) instead of raw JSON. Both compile to the same request. ================================================================================ 4. KEY NOTES ================================================================================ - REQUIRED BODY FIELDS. `content.text`, `content.metadata` and `identityLinks[].confidence` are all required. Omitting any of them currently returns a 500 from ackDB rather than a clean validation error, which is easy to misread as an outage. If ingest 500s, check these three first. - SCHEMA IS PER SOURCE. `content.metadata` is validated against the selected pack. A missing required field, or a field of the wrong type, fails the whole event with a 422 naming the field. Read the pack's schema (ackDB's /schemas/sources/{source}) before mapping. - PINNING TO A HUMAN (person rails). By default ackDB works out the event's actor from the event's own contact mapping. A rail overrides that and binds the event to a specific human. The normal way to get one is ackdb_entity_lookup. The two rails are NOT interchangeable: contactId Pins the affiliation edge (this person AT this company) and, implicitly, the person behind it. NEVER mints anything — an unknown contactId is an error, not a create. personId Pins the HUMAN. Contact resolution then runs normally at the resolved company, so it MAY mint the edge. This is the sanctioned "same human, new company" path. Blank rails are DROPPED, which is what makes lookup-first safe: Lookup HIT -> the rail pins the event to that contact. Lookup MISS -> the lookup returns a BLANK contactId. The blank rail is dropped, ackDB resolves the actor from the event's mapping and mints the contact if new. The event lands. So `"contactId": "{{lookup.contactId}}"` is safe to wire unconditionally — it pins when there is someone to pin to, and degrades to normal resolution when there is not. A MALFORMED (non-blank, non-ULID) rail is NOT dropped — it fails the event loudly. That is deliberate: a garbage rail means the workflow is mis-wired, and silently ignoring it would attach the event to the wrong human. Rails fold into the dedup key: the same content pinned to a DIFFERENT actor is a different write, not a duplicate. Bodies with no rails hash exactly as before, so adding rails to a workflow causes no dedup drift. - RAIL REJECTIONS. All are 422 SCHEMA_VALIDATION_ERROR, raised BEFORE any write (so nothing is half-applied): contactId must be a 26-character ULID contactId not found: 01JZ… contactId 01JZ… belongs to entity 01JZ…, not the resolved entity 01JZ… (an edge cannot lie about its company) personId not found: 01JZ… contactId 01JZ… belongs to person 01JZ…, contradicting personId 01JZ… (the two rails disagree) contactId/personId are only valid on person-scope event types (check the event type's scope before wiring a rail) contactId/personId require entity resolution — provide a domain identity link or entityId And one 409 IDENTITY_CONFLICT: the body's own durable keys contradict the rail (e.g. a linkedin_url in the payload belongs to a different person than the pinned one). The write rolls back, and ackDB records a merge candidate you can review via its /persons/merge-candidates. A rail is a strict assertion — ackDB refuses rather than guessing. - DEDUPLICATION is on ackDB's side. Re-sending the same event returns `deduplicated: true` and writes nothing, so re-running a workflow over the same rows is safe. - A BLANK domain is dropped rather than sent, so an empty `identityLinks` value cannot mint a junk company keyed on "". - Ingest is free — 0 credits. ================================================================================ 5. WHERE IT FITS IN A WORKFLOW ================================================================================ Straight ingest from a source table: -> ackdb_ingest Lookup-first ingest — attribute the event to a known human: ackdb_entity_lookup (contact) -> ackdb_ingest (body sets "contactId": "{{lookup.contactId}}") A lookup MISS fails the row as a business failure ("No data found"), so whether the ingest still runs for that row is governed by the lookup action's `continue_workflow_if_action_fails`. Set it explicitly to `true` — the unmatched rows are exactly the ones you want to reach ingest, so ackDB can mint the contact. Ingest, then set hand-owned fields: ackdb_ingest -> ackdb_update_record (region, tier, account_owner) ================================================================================ 6. WHEN TO USE ================================================================================ Use ackdb_ingest to record something that HAPPENED — an email, a meeting, a payment, a signup, a usage event. Events are append-only and carry a timestamp; ackDB resolves the company and person from them and builds the timeline. ================================================================================ 7. WHEN NOT TO USE ================================================================================ Setting static, hand-owned data (region, tier, owner, custom attributes) -> ackdb_update_record. Those are properties of the record, not events, and ingest cannot set them. (https://floqer.com/docs/action-detail/ackdb_update_record.txt) Reading a company or contact out of ackDB -> ackdb_entity_lookup (https://floqer.com/docs/action-detail/ackdb_entity_lookup.txt) ================================================================================ This file is maintained manually. Last updated: 2026-07-13. Full interactive reference: https://floqer.com/docs/reference Action catalog: https://floqer.com/docs/action-catalog.txt