App nodes work with other services through a credential: pick the credential, pick the operation, fill its fields (only that operation's fields show). They need the plan feature integration.
Every app node runs on one shared runner (lib/integration): the operation's request, signed with the credential's auth headers and query; its pages followed up to Results at most (100); its answer turned into items (one per row, record, message…). A write (sending, creating, deleting) in a test run isn't sent: it comes out as { dry: true, operation }. A refused call fails the step with the service's answer, so On error applies.
Contents
Telegram
integration/telegram · credential: Telegram bot
| Operation | Fields | Comes out |
|---|
message.send | chat id, text, formatting (none, Markdown, HTML) | the sent message |
message.photo | chat id, photo (URL or Telegram file id), caption | the sent message |
message.document | chat id, the item's file, caption | the sent message |
message.edit / message.delete | chat id, message id (and text) | the message / { isDeleted } |
chat.get | chat id | the chat |
update.get | how many | the bot's latest updates (messages people sent it: their chat ids) |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (telegram/bot) | | |
operation | Operation | select: message.send, message.photo, message.document, message.edit, message.delete, chat.get, update.get | "message.send" | |
chatId | Chat id | text | | operation: message.send, message.photo, message.document, message.edit, message.delete, chat.get |
text | Text | textarea | | operation: message.send, message.edit |
parseMode | Formatting | select: none, Markdown, HTML | "none" | operation: message.send |
photo | Photo (URL or file id) | text | | operation: message.photo |
caption | Caption | textarea | | operation: message.photo, message.document |
file | The item's file (key) | text | "data" | operation: message.document |
messageId | Message id | text | | operation: message.edit, message.delete |
limit | Results at most | number | 20 | operation: update.get |
limit | Results at most | number | 100 | |
Discord
integration/discord · credential: Discord webhook (for webhook.send) or Discord bot (the others)
| Operation | Fields |
|---|
webhook.send | message, name shown, embed (Discord JSON) |
message.send / message.list / message.delete / message.react | channel id (and message, message id, emoji) |
channel.get | channel id |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (discord/webhook, discord/bot) | | |
operation | Operation | select: webhook.send, message.send, message.list, message.delete, message.react, channel.get | "webhook.send" | |
content | Message | textarea | | operation: webhook.send, message.send |
username | Name shown | text | | operation: webhook.send |
embed | Embed (JSON) | json | | operation: webhook.send |
channelId | Channel id | text | | operation: message.send, message.list, message.delete, message.react, channel.get |
limit | Results at most | number | 50 | operation: message.list |
messageId | Message id | text | | operation: message.delete, message.react |
emoji | Emoji | text | "👍" | operation: message.react |
limit | Results at most | number | 100 | |
Slack
integration/slack · credential: Slack token
| Operation | Fields |
|---|
message.send | channel, text, reply in thread (ts) |
message.update / message.delete | channel, message ts (and text) |
reaction.add | channel, message ts, emoji |
channel.list / channel.history | (channel); pages by cursor |
user.get | user id |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (slack/token) | | |
operation | Operation | select: message.send, message.update, message.delete, reaction.add, channel.list, channel.history, user.get | "message.send" | |
channel | Channel | text | | operation: message.send, message.update, message.delete, reaction.add, channel.history |
text | Text | textarea | | operation: message.send, message.update |
thread | Reply in thread (ts) | text | | operation: message.send |
ts | Message ts | text | | operation: message.update, message.delete, reaction.add |
emoji | Emoji | text | "thumbsup" | operation: reaction.add |
limit | Results at most | number | 100 | operation: channel.list, channel.history |
user | User id | text | | operation: user.get |
limit | Results at most | number | 100 | |
Slack answers a refusal as ok: false: the step fails with its error (channel_not_found, not_in_channel…).
Notion
integration/notion · credential: Notion
| Operation | Fields |
|---|
page.create | database id, properties and blocks (Notion's JSON) |
page.get / page.update / page.archive | page id (and properties) |
database.query | database id, filter and sorts (Notion's JSON); one item per page |
block.append | page id, blocks |
search | text |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (notion/token) | | |
operation | Operation | select: page.create, page.get, page.update, page.archive, database.query, block.append, search | "page.create" | |
databaseId | Database id | text | | operation: page.create, database.query |
property | Properties (Notion JSON) | json | | operation: page.create, page.update |
children | Blocks (Notion JSON) | json | | operation: page.create, block.append |
pageId | Page id | text | | operation: page.get, page.update, page.archive, block.append |
filter | Filter (Notion JSON) | json | | operation: database.query |
sort | Sorts (Notion JSON) | json | | operation: database.query |
query | Search | text | | operation: search |
limit | Results at most | number | 100 | |
Share the database or page with your integration in Notion first.
Airtable
integration/airtable · credential: Airtable
| Operation | Fields |
|---|
record.list | base id, table, filter formula, view; pages by offset; one item per record ({ id, …fields }) |
record.get / record.create / record.update / record.delete | base id, table, record id, fields (JSON; values are typecast) |
base.list | — |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (airtable/token) | | |
operation | Operation | select: record.list, record.get, record.create, record.update, record.delete, base.list | "record.list" | |
base | Base id | text | | operation: record.list, record.get, record.create, record.update, record.delete |
table | Table (name or id) | text | | operation: record.list, record.get, record.create, record.update, record.delete |
formula | Filter formula | text | | operation: record.list |
view | View | text | | operation: record.list |
limit | Results at most | number | 100 | operation: record.list |
recordId | Record id | text | | operation: record.get, record.update, record.delete |
fields | Fields (JSON) | json | | operation: record.create, record.update |
limit | Results at most | number | 100 | |
Google Sheets
integration/sheets · credential: Google
| Operation | Fields |
|---|
row.get | spreadsheet id, range (Sheet1!A:Z), first row names the columns: one item per row, { row, <column>: value } |
row.append / row.update | spreadsheet id, range, values (JSON rows, [["Ann", 30]]) or the item's fields in a column order (name, age); values are entered as typed |
range.clear | spreadsheet id, range |
spreadsheet.create | title: { spreadsheetId, url } |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (google/oauth2) | | |
operation | Operation | select: row.get, row.append, row.update, range.clear, spreadsheet.create | "row.get" | |
spreadsheetId | Spreadsheet id | text | | operation: row.get, row.append, row.update, range.clear |
range | Range (Sheet1!A:D) | text | | operation: row.get, row.append, row.update, range.clear |
isHeader | First row names the columns | boolean | true | operation: row.get |
values | Values (JSON rows) | json | | operation: row.append, row.update |
columns | Or the item's fields (comma separated) | text | | operation: row.append, row.update |
title | Title | text | | operation: spreadsheet.create |
limit | Results at most | number | 100 | |
Google Drive
integration/drive · credential: Google
| Operation | Fields |
|---|
file.list | search in Drive's syntax (name contains 'invoice' and trashed = false); pages by token |
file.get / file.delete | file id |
file.download | file id: it becomes the item's data |
file.upload | the item's file, name, folder id |
folder.create | name, parent folder id |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (google/oauth2) | | |
operation | Operation | select: file.list, file.get, file.download, file.upload, file.delete, folder.create | "file.list" | |
query | Search | text | "trashed = false" | operation: file.list |
limit | Results at most | number | 100 | operation: file.list |
fileId | File id | text | | operation: file.get, file.download, file.delete |
key | File key in the item | text | "data" | operation: file.download |
file | The item's file (key) | text | "data" | operation: file.upload |
name | Name | text | | operation: file.upload, folder.create |
folderId | Folder id | text | | operation: file.upload, folder.create |
limit | Results at most | number | 100 | |
Gmail
integration/gmail · credential: Google
| Operation | Fields |
|---|
message.send | to, cc, bcc, subject, body (text or HTML), attachments (the item's file keys); the message is built as MIME, non-ASCII subjects encoded |
message.list | search (Gmail's syntax: from:x is:unread); { id, threadId } per message |
message.get | message id: { id, threadId, from, to, subject, date, snippet, label } |
message.trash | message id |
label.list | — |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (google/oauth2) | | |
operation | Operation | select: message.send, message.list, message.get, message.trash, label.list | "message.send" | |
to | To | text | | operation: message.send |
cc | Cc | text | | operation: message.send |
bcc | Bcc | text | | operation: message.send |
subject | Subject | text | | operation: message.send |
body | Body | textarea | | operation: message.send |
isHtml | Body is HTML | boolean | false | operation: message.send |
attachment | Attachments (the item's file keys) | text | | operation: message.send |
query | Search | text | | operation: message.list |
limit | Results at most | number | 50 | operation: message.list |
messageId | Message id | text | | operation: message.get, message.trash |
limit | Results at most | number | 100 | |
Google Calendar
integration/calendar · credential: Google
| Operation | Fields |
|---|
event.list | calendar id (primary), from and until (ISO times; from defaults to now); single events, by start time |
event.create | calendar id, title, start, end (ISO times with their offset), description, location, guests |
event.delete | calendar id, event id |
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (google/oauth2) | | |
operation | Operation | select: event.list, event.create, event.delete | "event.list" | |
calendarId | Calendar id | text | "primary" | operation: event.list, event.create, event.delete |
from | From (ISO time) | text | | operation: event.list |
until | Until (ISO time) | text | | operation: event.list |
limit | Results at most | number | 100 | operation: event.list |
title | Title | text | | operation: event.create |
start | Starts (ISO time) | text | | operation: event.create |
end | Ends (ISO time) | text | | operation: event.create |
description | Description | textarea | | operation: event.create |
location | Location | text | | operation: event.create |
attendee | Guests (emails, comma separated) | text | | operation: event.create |
eventId | Event id | text | | operation: event.delete |
limit | Results at most | number | 100 | |
Email (SMTP)
integration/email · credential: SMTP · an effect node
Sends an email through your SMTP server: to, cc, bcc, reply-to, subject, text or HTML body, and the item's files as attachments. From: the credential's From address, else its user. Output: { messageId, accepted }.
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (email/smtp) | | |
to | To | text | | |
cc | Cc | text | | |
bcc | Bcc | text | | |
replyTo | Reply to | text | | |
subject | Subject | text | | |
body | Body | textarea | | |
isHtml | Body is HTML | boolean | false | |
attachment | Attachments (the item's file keys) | text | | |
MCP call
integration/mcp · credential: MCP server
List tools (one item each: { name, description, schema }) or Call a tool with arguments (JSON): { name, text, json }, where json is its structured content. A tool that reports an error takes the error dot. Streamable HTTP; the session is kept for the call.
| Field | Label | Kind | Default | Shown for |
|---|
credential | Credential | credential (mcp/server) | | |
operation | Operation | select: tool.list, tool.call | "tool.call" | |
tool | Tool | text | | operation: tool.call |
argument | Arguments (JSON) | json | | operation: tool.call |
Add an integration
An integration is a folder src/lib/flow/node/integration/<service>/ with operation.ts (its operations: fields, request, how the answer becomes items, pagination, whether it writes), index.ts (the node: integrationField(…) builds its fields) and run.ts (export default integrationRun(…)), plus a line in the node list and its labels under component.flow.node.integration.<service>. An operation that isn't one request declares run instead.
Not yet: polling triggers (new rows, new mail) and app webhooks.