DocumentationProvider-aware function helpers

Provider-aware function helpers

Build Slack, Discord, Telegram, Notion, Airtable, Google Sheets and Supabase requests, or enrich webhooks with managed or customer-provided AI.

Provider-aware helpers remove repetitive payload mapping from Functions. With one exception, they are pure builders: they return a body or request descriptor and never contact the destination. Apply that result to an output so retries, delivery logs, throttling and response functions continue to work.

ai.generate() is the exception. It performs a bounded synchronous enrichment request through a typed AI provider connection.

Webhook Relay AI

Create a Webhook Relay AI connection and bind it to ai:writer. It needs no API key, selects the model for you, and shares the account agent's monthly AI token allowance:

const ai = require("ai:writer")
const result = ai.generate("Summarize: " + r.body, {
  maxTokens: 200
})
r.setBody(JSON.stringify({ summary: result.text, usage: result.usage }))

Do not pass model or temperature for Webhook Relay AI. The platform manages those settings.

OpenAI

Create an OpenAI service connection and bind it to ai:writer:

const ai = require("ai:writer")
const result = ai.generate("Summarize: " + r.body, {
  model: "gpt-5-mini",
  maxTokens: 200
})
r.setBody(JSON.stringify({ summary: result.text, usage: result.usage }))

Google Gemini

The same helper uses Gemini's generateContent API when the alias is bound to a Gemini connection:

const ai = require("ai:gemini")
const result = ai.generate("Extract the customer intent from: " + r.body, {
  model: "gemini-2.5-flash",
  maxTokens: 100
})
r.setHeader("X-Customer-Intent", result.text.trim())

Anthropic

Bind the alias to an Anthropic connection and supply an Anthropic model name:

const ai = require("ai:reviewer")
const result = ai.generate("Review this event for anomalies: " + r.body, {
  model: "claude-haiku-4-5",
  system: "Be concise.",
  maxTokens: 200
})
r.setBody(JSON.stringify({ review: result.text }))

Custom Chat Completions API

A custom connection requires an HTTPS base URL whose API implements OpenAI's POST /chat/completions request and response shape:

const ai = require("ai:private-model")
const result = ai.generate("Label this event: " + r.body, {
  model: "company-classifier-v2",
  maxTokens: 32,
  timeoutMs: 5000
})
r.setHeader("X-Label", result.text.trim())

API keys for all four customer-provided AI backends stay in the host runtime. Function code can call generate but cannot read the key. See AI provider connections for setup, structured JSON output and limits.

If the alias is missing, the account agent links you to the Function's Connections tab. Webhook Relay AI can be created there without credentials; enter customer-provider credentials only in the write-only Service Connections dialog, never in agent chat.

Slack

Use slack.message() for text or slack.blocks() for Block Kit. Configure a standard public output with the Slack incoming webhook URL as its destination; the helper only builds its JSON body. This is distinct from the preformatted Slack notification output, which wraps arbitrary webhook bodies for you.

const slack = require("slack")
const event = JSON.parse(r.body)

const body = slack.blocks([
  { type: "section", text: { type: "mrkdwn", text: "*Deploy complete*" } },
  { type: "context", elements: [{ type: "mrkdwn", text: event.version }] }
], { text: "Deploy complete: " + event.version })

r.setBody(JSON.stringify(body))
r.setHeader("Content-Type", "application/json")

Slack message payloads accept at most 50 blocks. options.text is required as the notification and accessibility fallback. When delivering to Slack's chat.postMessage API instead of an incoming webhook, pass the required channel as an option, for example slack.message("Build passed", {channel: "C123"}).

Discord

discord.embed() validates Discord's title, description, field and aggregate text limits. Wrap the returned embed in the webhook API's embeds array and use a standard public output targeting the Discord webhook URL:

const discord = require("discord")
const embed = discord.embed("Deploy complete", {
  description: "Production is healthy",
  color: 3066993,
  fields: [{ name: "Version", value: "2026.09.09", inline: true }]
})

r.setBody(JSON.stringify({ embeds: [embed] }))
r.setHeader("Content-Type", "application/json")

Telegram

The Telegram helpers return method, path and body. Apply all three to a standard HTTP output whose destination is https://api.telegram.org/bot<TOKEN>:

const telegram = require("telegram")
const request = telegram.message("-1001234567890", "Build passed", {
  disable_notification: true
})

r.setMethod(request.method)
r.setPath(request.path)
r.setBody(JSON.stringify(request.body))
r.setHeader("Content-Type", "application/json")

For an inline-button webhook, use telegram.answerCallback(event.callback_query.id, {text: "Done"}) and apply the returned descriptor in the same way. The helper does not include a bot token. Telegram authentication is part of the Bot API URL, and there is not currently a dedicated Telegram output connection, so the configured output destination contains the token.

Notion

notion.properties() maps ordinary values into typed Notion page properties. Use the result as properties in a page create or update output:

const notion = require("notion")
const event = JSON.parse(r.body)

const properties = notion.properties({
  Name: { type: "title", value: event.customer.name },
  Status: { type: "status", value: "New" },
  Amount: { type: "number", value: event.amount },
  Received: { type: "date", value: event.created_at }
})

r.setBody(JSON.stringify({
  parent: { type: "data_source_id", data_source_id: "DATA_SOURCE_ID" },
  properties
}))
r.setHeader("Content-Type", "application/json")
r.setHeader("Notion-Version", "2026-03-11")

Store the Notion integration token as a secret connection and set its Bearer header in the Function or output configuration.

Airtable

airtable.fields() wraps a record for Airtable and can enable API typecasting:

const airtable = require("airtable")
const event = JSON.parse(r.body)

const body = airtable.fields({
  Name: event.customer.name,
  Email: event.customer.email,
  Active: true
}, { typecast: true })

r.setBody(JSON.stringify(body))
r.setHeader("Content-Type", "application/json")

This example creates one record. Use an Airtable personal access token from a secret connection for the Authorization: Bearer … header. The output destination remains the Airtable records endpoint.

Google Sheets

sheets.row() creates a ValueRange with stable column order for the Sheets spreadsheets.values.append endpoint:

const sheets = require("sheets")
const event = JSON.parse(r.body)

const row = sheets.row(event, ["id", "email", "amount", "created_at"])
r.setBody(JSON.stringify(row))
r.setHeader("Content-Type", "application/json")

Missing object properties become null cells. Authentication and the sheet range belong to the configured output; the helper does not append the row.

Supabase

supabase.insert() and supabase.upsert() return a PostgREST request descriptor. Apply its method, path, optional raw query, headers and body to an output targeting the Supabase project URL:

const supabase = require("supabase")
const request = supabase.upsert("events", JSON.parse(r.body), {
  onConflict: "id",
  returning: "representation"
})

r.setMethod(request.method)
r.setPath(request.path)
if (request.rawQuery) r.setRawQuery(request.rawQuery)
Object.keys(request.headers).forEach(name => r.setHeader(name, request.headers[name]))
r.setBody(JSON.stringify(request.body))

Import the Supabase key from a secret connection and set the apikey and Authorization headers, or configure those headers on the output. Keeping the write as an output makes an upsert observable and replayable through the normal delivery system.

Lua usage

Every builder uses the same module and camelCase method names in Lua. Tables replace JavaScript objects and request setters use colon syntax:

local slack = require("slack")
local json = require("json")

local body = slack.message("Build passed")
local encoded, err = json.encode(body)
if err then error(err) end

r:SetRequestBody(encoded)
r:SetRequestHeader("Content-Type", "application/json")

The generated Function reference in the editor includes runnable JavaScript and Lua examples for every method, including telegram.answerCallback() and both Supabase write builders.

Next steps

Did this page help you?