Skip to main content

Updated Sep 30, 2026

Functions reference

CALL steps invoke functions by function_id. Each function validates its arguments; failures surface as step errors (the run stops along that path).

Build with AI

Use Vibe coding so an assistant can query the semantic schema and wire arguments safely.

For AI assistants

Argument and return shapes, edge cases, and SQL dialect notes live in the flow authoring guide.

Data​

semantic.query​

Runs SQL against the workspace semantic database (accounts, metrics, conversations, tickets, etc.).

Args: query (required); params (optional) — an array of positional values bound to ? or $1 placeholders in query, so you don't have to splice values into the SQL string yourself.

Returns: { "results": [...], "columns": [...], "total_rows": N }

{
"function_id": "semantic.query",
"args": {
"query": "SELECT account_id, name, health_score FROM accounts WHERE churned = 0 LIMIT 100"
}
}

Common tables include accounts, meetings, conversations, tickets, topics, notes, tasks, activities, contacts, workspace_users, account_metrics_latest (latest value per metric), account_metrics_history (metric time series), calendar_events and calendar_event_accounts (upcoming meetings from connected Google Calendars), outcome_programs, cohorts, and cohort_accounts, content_labels and content_themes (content themes), facts (extracted account facts), and dataset_records('your_dataset') for dataset-backed tables. Column lists and types are documented in the flow authoring guide.

data_connection.query​

Runs SQL against a configured external connection (warehouse, CRM mirror, etc.).

Args: data_connection_id, query
Returns: Same general shape as semantic.query.

{
"function_id": "data_connection.query",
"args": {
"data_connection_id": "019b3c9e-aaaa-bbbb-cccc-ddddeeeeffff",
"query": "SELECT id, name FROM companies WHERE updated_at > current_timestamp - interval '1' day"
}
}

data_connection.request​

Makes an authenticated HTTP request to a vendor's API using a workspace connection's stored credentials — useful for action-only connections (for example DevRev) that don't back a data model, or for calling an endpoint a model doesn't cover.

Args: data_connection_id, method (GET/POST/PUT/PATCH/DELETE), path (no scheme or host — the connection supplies the base URL), optional query and headers, and at most one body form:

Body formUse it for
bodyA JSON body. Content-Type isn't set for you, so add it to headers when the vendor requires it.
raw_bodyNon-JSON content: { "data": "...", "content_type": "text/csv", "encoding": "utf8" }. content_type is required; encoding is utf8 (default) or base64. A Content-Type in headers as well fails the step.
multipartForm uploads: { "fields": { "name": "value" }, "files": [{ "field": "...", "filename": "...", "data": "...", "content_type": "...", "encoding": "base64" }] }. Include at least one field or file. Each file needs field, filename, and data; file encoding defaults to base64. FunnelStory sets the multipart Content-Type, so a Content-Type in headers fails the step.

Setting more than one body form fails the step.

Headers: Vendor headers you set (for example X-Atlassian-Token) are forwarded. Authorization, Cookie, Host, Accept-Encoding, Expect, and hop-by-hop headers such as Connection are dropped; authentication always comes from the connection.

Returns: { "status_code": N, "body": ... } — non-2xx responses are still returned, not treated as errors; only transport/read failures raise a step error.

Test runs: Only GET requests run, and any body form is rejected, so uploads and writes can only be checked in a live run.

{
"function_id": "data_connection.request",
"args": {
"data_connection_id": "019b3c9e-aaaa-bbbb-cccc-ddddeeeeffff",
"method": "GET",
"path": "/works",
"query": { "id": "{{ $.devrev_issue_id }}" }
}
}

Uploading a file generated by an earlier step (for example pptx.create, which returns base64 in data):

{
"function_id": "data_connection.request",
"args": {
"data_connection_id": "019b3c9e-aaaa-bbbb-cccc-ddddeeeeffff",
"method": "POST",
"path": "/api/v2/task/{{ $.task_id }}/attachment",
"multipart": {
"files": [
{
"field": "attachment",
"filename": "weekly-status-v1.pptx",
"data": "{{ $.deck.data }}",
"content_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
}
]
}
}
}

accounts.select​

Returns account IDs matching a filter group (AND of OR groups of filters)—the same structured filters used elsewhere in FunnelStory.

Returns: { "total_count": N, "account_ids": ["..."] }

{
"function_id": "accounts.select",
"args": {
"filter": {
"and_group": [
{
"or_group": [
{
"filter": {
"name": "",
"metric_filter": {
"metric_id": "product_engagement",
"condition": "equal",
"value": "daily_active"
}
}
}
]
}
]
}
}
}

Metric and activity IDs are workspace-specific—look them up in your configuration; do not guess.

Datasets​

dataset.record.upsert​

Creates or replaces a record keyed by string key.

Returns: { "dataset": "...", "key": "..." }

{
"function_id": "dataset.record.upsert",
"args": {
"dataset": "qbr_notes",
"key": "{{ $.account_id }}",
"record": {
"summary": "$.llm_output",
"updated_at": "{{ $.now }}"
}
}
}

Use quoted "$.var" form when passing whole objects; use {{ $.id }} for string interpolation inside SQL or text.

dataset.record.set_field​

Updates a single field on an existing record.

dataset.record.delete​

Deletes a record by key.

Communication​

slack.send_message​

Required: connection_id, channel_id, and at least one of text or blocks.

Optional: thread_ts (reply in a thread); attachments — an array of files (filename, data, optional title; up to 10 files, 25 MB total) to share instead of a plain message. data accepts plain text, raw base64, or a data:<mime>;base64,... string. attachments and blocks are mutually exclusive — when you attach files, any text becomes the caption on the file share rather than a separate message.

Returns: { "success": true, "response_channel": "...", "response_timestamp": "..." } (response_timestamp is omitted for file-attachment sends).

{
"function_id": "slack.send_message",
"args": {
"connection_id": "slack_conn_123",
"channel_id": "C01234567",
"text": "{{ $.message }}"
}
}

With a file attachment:

{
"function_id": "slack.send_message",
"args": {
"connection_id": "slack_conn_123",
"channel_id": "C01234567",
"text": "This week's QBR summary",
"attachments": [
{ "filename": "qbr-summary.pdf", "data": "{{ $.report_base64 }}" }
]
}
}

slack.list_users​

Lists the users in a Slack workspace, optionally filtered by a search term matched against name, real name, or email. Bots, deleted, and restricted users are excluded.

Args: connection_id, optional search

Returns: { "users": [{ "id": "...", "name": "...", "real_name": "...", "email": "..." }] }

{
"function_id": "slack.list_users",
"args": {
"connection_id": "slack_conn_123",
"search": "priya"
}
}

slack.open_conversation​

Opens a direct message (one user) or group DM (2–8 users), returning a channel id you can pass to slack.send_message.

Args: connection_id, user_ids (1–8 Slack user IDs)

Returns: { "channel_id": "...", "is_mpim": false }

email.send​

Required: to, subject, body (plain text). html is optional; body is still required.

Optional: cc, bcc, send_separately, data_connection_id, from, reply_to_message_id, attachments.

How the email is delivered depends on which of from and data_connection_id you set:

You setDelivery
NeitherFunnelStory's no-reply address.
from onlyThat workspace member's connected Gmail account. from must match the Gmail address they connected; the step fails if they haven't connected Gmail.
data_connection_id and fromThe connected email provider, sending as from.
data_connection_id without fromError — from is required with a connection.

reply_to_message_id sends the email as a reply inside an earlier email's thread. Pass the message_id returned by an earlier email.send. It works only for Gmail sends (from without data_connection_id) from the same mailbox that sent the original, and can't be combined with send_separately. The reply's subject is always the original's with a Re: prefix, and recipients aren't copied from the original — pass to and cc again.

Returns: { "sent": true, "recipients": [...], "recipient_count": N, "emails_sent": M, "message_id": "..." } — message_id identifies the sent message for a later reply_to_message_id. It's omitted with send_separately, or when the provider doesn't report one.

{
"function_id": "email.send",
"args": {
"cc": ["[email protected]"],
"bcc": [],
"send_separately": false,
"subject": "Weekly summary",
"body": "Your weekly summary is ready",
"html": "<p>Your weekly summary is ready</p>"
}
}

Connection-backed send (requires data_connection_id and from):

{
"function_id": "email.send",
"args": {
"data_connection_id": "019b3c9e-...",
"from": "[email protected]",
"subject": "Weekly summary",
"body": "Your weekly summary is ready"
}
}

CRM​

salesforce.read_record / salesforce.update_record​

Args: data_connection_id, object_type, record_id, plus fields array for reads or fields object for updates.

salesforce.create_record​

Args: data_connection_id, object_type (sObject API name), fields (non-empty object — create body; values may be strings, numbers, booleans, or nested objects where Salesforce accepts them).

Returns: { "id": "<Salesforce Id>", "ok": true }

hubspot.read_record / hubspot.update_record​

Args: data_connection_id, object_type (e.g. companies), record_id, and fields as above.

Utility​

tasks.create​

Creates a FunnelStory task. Title can be driven from template fields; see the flow authoring guide for optional priority, assignee, due date, actions, and references.

template.render​

Renders a stored message template by template_id with a vars map.

search.web​

Runs a web search; optional recency_filter: 7d, 30d, 90d, 1y.

http.request​

Makes a GET request to any public https URL — no connection or authentication required. Use it to pull a public API response or download a public file into the flow.

Args: url (absolute https URL, including any query string)

Returns: { "status_code": N, "body": ... } when the response is JSON, or { "status_code": N, "body_base64": "..." } otherwise.

Responses are capped at 8 MB, and requests to private, loopback, or internal-network hosts are rejected (including redirect targets) — this function can only reach the public internet.

{
"function_id": "http.request",
"args": {
"url": "https://api.example.com/status?id={{ $.record_id }}"
}
}