Trigger events
A trigger turns something happening at a provider into a durable workflow run. What reaches your workflow is not the provider's payload: it is a normalized event, the flat set of fields listed per trigger below. Your mapping reads those fields and nothing else, so this page is the contract.
Two columns need a word. Always there says whether the field is guaranteed: one marked no arrives as a key with nothing in it, so a workflow that reads through it without checking throws inside the step. Type follows the declared schema, where number covers an integer and string or null means the provider sends both.
Turn a link on in Triggers under Integrations in Studio. It starts from the moment you turn it on, so anything that happened while it was off is not delivered.
When your workflow writes back through the same connection
A trigger delivers every change to an object, or every new object of a kind. If your workflow answers a delivery by writing back through the same connection, that write causes exactly such a change. The next poll delivers it, your workflow runs again, and it writes again.
Deduplication cannot stop this, because nothing is being duplicated. A trigger that follows a change column counts a second change to one object as a genuinely new delivery. A trigger that watches for new objects gets a new id every lap.
The brake is isOwnEcho. It compares a value your own write returned against the value the delivery carried, and it goes before anything is written. Both halves have to be there: your write records the value by naming echoPath, and the guard reads it back. The cautions further down this page say "guard inside the workflow body". They mean your own code rather than the delivery path, and the step that does the write is where it goes.
import { stackbone } from '@stackbone/sdk';
import { isOwnEcho } from '@stackbone/sdk/workflow';
import { z } from 'zod';
export const inputSchema = z.object({ chargeId: z.string() });
export const outputSchema = z.object({ action: z.enum(['answered', 'skipped_own_echo']) });
export async function refundOnRequest(input: z.infer<typeof inputSchema>) {
'use workflow';
return await answerCharge(input);
}
async function answerCharge(
input: z.infer<typeof inputSchema>,
): Promise<z.infer<typeof outputSchema>> {
'use step';
// The brake, before anything is written.
if (await isOwnEcho('stripe', input.chargeId)) {
return { action: 'skipped_own_echo' };
}
await stackbone
.connection('stripe')
.call('create_refund', { charge: input.chargeId }, { echoPath: 'charge' });
return { action: 'answered' };
}Which value to compare is the part that decides whether the guard works, and it follows the trigger:
- A trigger that watches for new objects: compare the new object’s id.
- A trigger that follows a change column such as
updatedAt: compare that column. An id never changes, so a guard on the id would read every later edit of that object as your own echo and swallow a person’s change with it. - Never compare the id of the event itself. That id is new on every lap, so the guard matches nothing and stops nothing.
The example above, on a Stripe New Refund link, is the first case with a twist, and it carries a cost worth knowing. Each lap delivers a new refund with a new id, so the guard compares the charge the refund names, the only value both sides share. While that id sits in the guard’s window, a second refund on the same charge reads as your own echo, even when a person made it. Binding the workflow to a trigger your own writes do not raise is the other half of the answer.
A match is workspace-wide and it expires. A write is yours whichever workflow made it, so two workflows working on one object count as one writer. The guard also only remembers for a window, so a delivery that arrives long after the write is not recognised.
If the path resolves to nothing the write records a marker instead of a value, and the next isOwnEcho throws echo_guard_unavailable rather than answering "not ours". A failed run in front of you beats a loop behind your back. Fix the echoPath on the write that left the marker, and note that it is not always your own: a write with no workflow owner, such as one made inside an agent turn’s tool, blinds every caller on that connector. Retrying does not help, because the marker lives until its row ages out.
Where the per-connector path is. Most triggers below name the exact echoPath in their own caution. Some do not, for three different reasons: GitHub’s issue and pull request events name the account that made the change, so their guard reads that instead; a connector that exposes no write operation has nothing for the guard to compare against; and on some, nothing the connector writes raises the event the trigger delivers.
The box warns when the guard is missing. A run that a link started may write back through that link’s own connector with an echoPath and never ask isOwnEcho about that connector. The link then shows one warning, however many runs did it: a banner on the trigger’s page in Studio, and a warning: line under the link in stackbone triggers list. The warning names the connector and the operation, and it says the link may start itself, because the box cannot see which object the write changed. A file written to a folder the trigger does not watch makes no loop, and still gets the warning. The warning covers the last hour, so it goes away when the runs stop writing without the guard. A run that asked isOwnEcho about that connector is not counted, whatever the guard answered. A call that names no echoPath counts as a read and raises nothing, so the warning cannot see a write-back that records no echo.
GitHub
Served by the Activepieces GitHub piece, so the piece's own code decides what each trigger delivers. Its triggers are webhooks: turning a link on creates a hook on the repository pointed at the link, turning it off deletes the hook, and each event GitHub sends starts a run as it arrives. The connection's token therefore needs the right to manage the repository's hooks (admin:repo_hook on a classic token), and an event GitHub sent while the box was down never arrives: GitHub does not deliver it again on its own. Each trigger asks for the Repository. New Commit and New Issue replaced GitHub's earlier New commit by date and Issue updated triggers. The event is GitHub's own payload, not a reduced copy: a mapping written for New commit by date reads id for sha, author.name for authorName and author.email for authorEmail, and one written for Issue updated reads issue.id, issue.number, issue.title, issue.state and issue.updated_at. The trigger's sample in Studio shows the exact shape.
New Commit
github · new_commit
Starts a run for each new commit a push brings to any branch of the repository. A commit GitHub has seen before, such as one fast-forwarded from another branch, starts nothing, and neither does a tag push or a branch deletion. The event is the commit as the push lists it: id is its sha, with message, timestamp, url, author and committer. It does not say which branch it went to: arm Push to filter on the branch. The box keys each delivery on GitHub's delivery id, and on the commit's sha too when a push brings several, so a redelivered push starts nothing twice.
Caution
a workflow that commits to the repository through this connection starts itself again, whichever branch the commit lands on: the trigger watches every branch. Guard inside the workflow body with isOwnEcho on the commit's sha. Pass echoPath: 'commit.sha' on the create_or_update_file or delete_file call, then open the workflow with if (await isOwnEcho('github', event.id)) return;. A commit the workflow pushes any other way is not covered. A mapping cannot skip an event: every delivery starts a run.
Push
github · trigger_push
Starts one run per push to the repository: a branch or a tag, created, updated or deleted. The event is GitHub's push: ref names the branch or tag, before and after the shas around the push, commits the commits it brought, deleted whether it removed the ref. A workflow that watches one branch returns early when ref is another. Once per push: the box keys each delivery on GitHub's delivery id.
Caution
a workflow that commits to the repository through this connection starts itself again, whichever branch the commit lands on: the trigger watches every branch. Guard inside the workflow body with isOwnEcho on the commit's sha. Pass echoPath: 'commit.sha' on the create_or_update_file or delete_file call, then open the workflow with if (await isOwnEcho('github', event.after)) return;. A commit the workflow pushes any other way is not covered. A mapping cannot skip an event: every delivery starts a run.
New Issue
github · trigger_issues
Starts a run for every change GitHub reports on an issue of the repository, not only a new one: opened, edited, closed, reopened, labeled, assigned and the rest. action names the change, issue is the issue after it, and sender the account that made it. A comment is not one of these changes: GitHub reports comments separately, and this trigger does not receive them. Neither does it receive pull requests: Pull Request does. Once per change: the box keys each delivery on GitHub's delivery id.
Caution
a workflow that writes back to the issue it received (a label, an edit, a close) starts itself again: GitHub sends that change as a new event. Every event names the account that made the change in sender.login, so return early from the workflow body when it is the account the connection acts as. Make that account a bot or a GitHub App, because the guard also skips every change it makes by hand. A mapping cannot skip an event: every delivery starts a run.
Pull Request
github · trigger_pull_request
Starts a run for every change GitHub reports on a pull request of the repository: opened, edited, closed, reopened, synchronized by a new push and the rest. action names the change, pull_request is the pull request after it, and sender the account that made it. A merge arrives as closed with pull_request.merged set. Once per change: the box keys each delivery on GitHub's delivery id.
Caution
a workflow that writes back to the pull request it received (a label, an edit, a merge) starts itself again: GitHub sends that change as a new event. Every event names the account that made the change in sender.login, so return early from the workflow body when it is the account the connection acts as. Make that account a bot or a GitHub App, because the guard also skips every change it makes by hand. A mapping cannot skip an event: every delivery starts a run.
Gmail
Served by the Activepieces Gmail piece, so the piece's own code decides what the trigger delivers. It replaced Gmail's earlier Message received trigger. The event is the message parsed, not Gmail's resource: message holds what the mail says, thread the thread Gmail answered. A mapping written for Message received reads message.text for snippet, message.from.text for from and message.subject for subject. id and threadId are Gmail's own ids for the message and its thread; message.messageId is the mail's Message-ID header, which the sender writes and may leave out.
New Email
gmail · gmail_new_email_received
Starts a run for each message Gmail received since the last poll, oldest first, in any label but Spam and Trash: mail the connected account sends is new mail too. The link may narrow it by sender, recipient, subject, label or category; a label is given by its name. Each poll reads every message since the last one, up to 500. Once per message: the box keys each delivery on the event's id.
Caution
a workflow that sends or replies through this connection starts itself again. The trigger delivers sent mail as well as the inbox, so the workflow's own message is new mail to it. Guard inside the workflow body with isOwnEcho on the event's id, Gmail's message id: pass echoPath: 'data.id' on the send_email or reply_to_email call, then open the workflow with if (await isOwnEcho('gmail', event.id)) return;. Never guard on threadId: it would also swallow the customer's next message in that thread. A mapping cannot skip an event: every delivery starts a run.
The event
| Field | Type | Always there |
|---|---|---|
id |
string | yes |
threadId |
string | yes |
message.subject |
string | no |
message.from |
object (value, text, html) |
no |
message.to |
object (value, text, html) |
no |
message.date |
string, an ISO 8601 time | no |
message.messageId |
string | no |
message.text |
string | no |
message.textAsHtml |
string | no |
message.html |
string, or false with no HTML part |
yes |
message.attachments |
array of object (fileName, mimeType, size, data) |
yes |
message.headerLines |
array of object (key, line) |
yes |
thread.data |
object (id, historyId, messages) |
yes |
Google Drive
Served by the Activepieces Google Drive piece, so the piece's own code decides what each trigger delivers. The event is the file as Drive lists it, with id, name, mimeType, createdTime, modifiedTime and its link; the trigger's sample in Studio shows the exact shape. Each trigger asks for an optional Parent Folder and watches that folder itself, not its subfolders. Left empty, a trigger is not scoped to one folder.
New File
google-drive · new_file
Starts a run for each file created since the last poll. It decides on the file's createdTime alone, so a file moved into the folder, or one already there and only modified, starts nothing. Once per file: the box keys each delivery on the file's id.
Caution
a file this connection uploads to the watched folder starts the workflow again. Each upload creates a new file with a new id, so the trigger cannot tell it from a person's. Guard inside the workflow body with isOwnEcho, for a file the workflow uploads by calling drive_upload_file itself: pass echoPath: 'id' on that call, then open the workflow with if (await isOwnEcho('google-drive', event.id)) return;. The drive.file-write output writer (see Destinations) cannot carry an echo path, so a workflow that files its output through it is not covered and still needs separate input and output folders. A mapping cannot skip an event: every delivery starts a run.
New Folder
google-drive · new_folder
Starts a run for each folder created since the last poll, once per folder: the box keys each delivery on the folder's id.
New or Updated File
google-drive · new_or_updated_file
Starts a run for each file created or modified since the last poll, so it also catches an edit to a file that was already in the folder when the link was armed. Once per version of a file: the box keys each delivery on the file's id and its modifiedTime together, so every edit starts a run of its own, and a file listed again unchanged starts nothing. Several edits between two polls arrive as one delivery, the file as it stands at the later poll. A file moved into the folder starts nothing: a move changes neither stamp.
Caution
a workflow that writes to a file in the watched folder starts itself again. An upload is a new file, and an edit such as drive_replace_file_content or drive_update_file_metadata moves the file's modifiedTime, which this trigger delivers as a new version. Guard inside the workflow body with isOwnEcho on the event's modifiedTime, never on its id: a guard on the id would also swallow a person's edit to any file the workflow wrote within the guard's window. Drive's write calls answer without the file's modifiedTime, so read it back after the write with drive_get_file, passing fields: 'id,modifiedTime' and echoPath: 'modifiedTime', then open the workflow with if (await isOwnEcho('google-drive', event.modifiedTime)) return;. An edit a person makes between your write and that read is taken for yours. The drive.file-write output writer (see Destinations) cannot carry an echo path, so a workflow that files its output through it is not covered and still needs separate input and output folders. A mapping cannot skip an event: every delivery starts a run.
Linear
Served by the Activepieces Linear piece, so the piece's own code decides what each trigger delivers. Its triggers are webhooks: turning a link on creates a webhook in the Linear workspace pointed at the link, turning it off deletes it, and each event Linear sends starts a run as it arrives. The connection's API key must be allowed to create webhooks, and an event Linear sent while the box was down arrives only as far as Linear's own retries reach it. New Issue, New Comment and Updated Issue replaced Linear's earlier Issue created, Comment created and Issue updated triggers. The event is Linear's own webhook payload, not a reduced copy: action names the change, data is the issue or the comment, actor the account that made the change, and url its page. A mapping written for Issue created reads data.id for issueId, data.identifier, data.title, data.state.name for state and data.team.key for team. One written for Comment created reads data.id for commentId, data.body, data.user for author and data.issue. One written for Issue updated also reads data.updatedAt, and actor for lastActor. Once per delivery: the box keys each one on Linear's Linear-Delivery header. The trigger's sample in Studio shows the exact shape.
New Issue
linear · new_issue
Starts a run for each issue created in the Team the link names. A link watches one team: a workspace with several arms one link per team. An edit or a removal starts nothing here.
Caution
a workflow that creates an issue this link watches starts itself again. Each new issue has a new id, so deduplication cannot stop the loop. Guard inside the workflow body with isOwnEcho on the new issue's id. On a rawGraphqlQuery call whose issueCreate selects issue { id }, pass echoPath: 'data.issueCreate.issue.id'; on linear_create_issue, pass echoPath: 'issue.id'. Then open the workflow with if (await isOwnEcho('linear', event.data.id)) return;. A mapping cannot skip an event: every delivery starts a run.
New Comment
linear · new_comment
Starts a run for each comment created on an issue in any of the workspace's public teams. A comment in a private team is not delivered, and an edited comment starts nothing. data.issue names the issue it was posted on, and data.user its author.
Caution
a workflow that creates a comment this link watches starts itself again. Each new comment has a new id, so deduplication cannot stop the loop. Guard inside the workflow body with isOwnEcho on the new comment's id. On a rawGraphqlQuery call whose commentCreate selects comment { id }, pass echoPath: 'data.commentCreate.comment.id'; on linear_create_comment, pass echoPath: 'comment.id'. Then open the workflow with if (await isOwnEcho('linear', event.data.id)) return;. A mapping cannot skip an event: every delivery starts a run.
Updated Issue
linear · updated_issue
Starts a run for each change to an issue, in the Team the link names, or in every public team when it names none. A new issue is not a change: arm New Issue beside it. A deleted issue arrives on Removed Issue instead. updatedFrom holds the fields that changed, with their values before the change, and actor who changed them.
Caution
a workflow that writes back to the issue it received starts itself again: Linear sends that change as a new update. Guard inside the workflow body with isOwnEcho on the issue's updatedAt, never on its id, which would also swallow every later edit a person makes. On a rawGraphqlQuery call whose issueUpdate selects issue { id updatedAt }, pass echoPath: 'data.issueUpdate.issue.updatedAt', then open the workflow with if (await isOwnEcho('linear', event.data.updatedAt)) return;. A mapping cannot skip an event: every delivery starts a run.
Write-back can also overwrite a person's edit made since the event was sent. issueUpdate exposes no version or expected-value precondition. Write only fields reserved for the workflow.
Microsoft Outlook
Served by the Activepieces Microsoft Outlook piece, so the piece's own code decides what each trigger delivers. One connector for both ways of signing in: a person's mailbox through OAuth, or a mailbox the connection names through client credentials. The mailbox is the connection's, never the link's. New Email replaced Outlook's earlier Message received trigger. The event is the message as Microsoft Graph answers it, not a reduced copy: a mapping written for Message received reads id for messageId, bodyPreview for snippet and receivedDateTime for date, and states the mailbox, which the event does not carry. The trigger's sample in Studio shows the exact shape.
New Email
microsoft-outlook · newEmail
Starts a run for each message received in the Inbox since the last poll, from every sender and to every recipient unless the link narrows it by From or To. Mail the connection sends is filed in Sent Items, not the Inbox, and starts nothing. Once per message: the box keys each delivery on the message's id.
Stripe
Served by the Activepieces Stripe piece, so the piece's own code decides what each trigger delivers. These replaced Stripe's earlier Stripe event trigger, which read the account's event feed on a schedule and took any event type. Each of these is one event type, and Stripe delivers it: arming a link creates a webhook endpoint at Stripe for that type, pointed at the link's URL, and switching the link off deletes it. The key needs write access to webhook endpoints, and a Stripe account holds at most 16 of them. The event is the object the Stripe event is about, not the event: a mapping written for the earlier trigger reads amount for data.object.amount, and the event's id, type and created are not on it. The box keys each delivery on Stripe's event id, so a delivery Stripe sends again starts nothing, and a second change to the same object starts its own run. Stripe retries a delivery that fails for up to three days in live mode; an event recorded before the link was armed never arrives. The piece does not check Stripe-Signature: the link's URL, and the token in it, is the credential. Test mode and live mode keep separate webhook endpoints, so a test key arms a test-mode endpoint and receives test events only.
New Payment
stripe · new_payment
Starts a run for each payment_intent.succeeded event: the payment intent.
Caution
a workflow on this trigger that creates a payment intent with confirm and a payment method starts itself again when the payment succeeds. Guard inside the workflow body with isOwnEcho: call create_payment_intent_ai with echoPath: 'id', then open the workflow with if (await isOwnEcho('stripe', event.payment_intent ?? event.id)) return;. An intent created without confirm succeeds later, in the customer's browser, and is not your own echo. A mapping cannot skip an event: every delivery starts a run.
New Customer
stripe · new_customer
Starts a run for each customer.created event: the customer.
Payment Failed
stripe · payment_failed
Starts a run for each charge.failed event: the charge, its failure_code and failure_message set.
New Subscription
stripe · new_subscription
Starts a run for each customer.subscription.created event: the subscription.
New Charge
stripe · new_charge
Starts a run for each charge.succeeded event: the charge. A refund raises no charge.succeeded, so a workflow here that refunds does not start itself again.
Caution
a workflow on this trigger that creates a payment intent with confirm and a payment method starts itself again when the payment succeeds. Guard inside the workflow body with isOwnEcho: call create_payment_intent_ai with echoPath: 'id', then open the workflow with if (await isOwnEcho('stripe', event.payment_intent ?? event.id)) return;. An intent created without confirm succeeds later, in the customer's browser, and is not your own echo. A mapping cannot skip an event: every delivery starts a run.
New Invoice
stripe · new_invoice
Starts a run for each invoice.created event: the invoice. The link may narrow it by status, customer or subscription.
Invoice Payment Failed
stripe · invoice_payment_failed
Starts a run for each invoice.payment_failed event: the invoice. Each failed attempt is its own event, so an invoice Stripe retries arrives once per attempt, attempt_count telling them apart. The link may narrow it by customer.
Canceled Subscription
stripe · canceled_subscription
Starts a run for each customer.subscription.deleted event: the subscription. The link may narrow it by customer.
New Refund
stripe · new_refund
Starts a run for each refund.created event: the refund, with the charge and payment_intent it gave money back on. The link may narrow it by either.
Caution
a workflow on this trigger that refunds through this connection starts itself again: every refund it makes is a new refund. Guard inside the workflow body with isOwnEcho: call create_refund with echoPath: 'charge', then open the workflow with if (await isOwnEcho('stripe', event.charge)) return;. Compare the charge, never the refund's own id, which is new on every lap. A mapping cannot skip an event: every delivery starts a run.
New Dispute
stripe · new_dispute
Starts a run for each charge.dispute.created event: the dispute. The link may narrow it by charge or payment intent.
New Payment Link
stripe · new_payment_link
Starts a run for each payment_link.created event: the payment link.
Updated Subscription
stripe · updated_subscription
Starts a run for each customer.subscription.updated event: the subscription as it is after the change. Each change is its own run. What changed is not on the event: Stripe's previous_attributes stays on the event the piece does not deliver. The link may narrow it by the new status or by customer.
Checkout Session Completed
stripe · checkout_session_completed
Starts a run for each checkout.session.completed event: the Checkout Session. The link may narrow it by customer.
Supabase
Served by the Activepieces Supabase piece, so the piece's own code decides what the trigger delivers. New Row replaced Supabase's earlier Row inserted or changed trigger, which read the table on a schedule: this one is delivered by Supabase itself, through a Database Webhook that calls the link. The event carries the inserted row under record, where the earlier trigger carried it under row, so a mapping written for it reads record.id for row.id. table keeps its name. There is no cursor column to choose: an edit to a row is never delivered.
New Row
supabase · new_row
Starts a run for each row a Supabase Database Webhook reports as inserted. Arming the link registers nothing at Supabase: in the Supabase dashboard, under Database and then Webhooks, create a hook on the table for the Insert event, of type HTTP Request with method POST, pointed at the link's URL. Neither Studio nor the CLI shows a link's URL yet, so a link on this trigger cannot be wired up yet. The link asks for the table and, optionally, the schema, and neither filters what arrives: the hook decides which table's inserts reach the link. A hook that also sends Update or Delete events gets an error back for each of those and starts nothing.
Supabase sends each delivery once and does not retry one that fails, so a row inserted while the box is down or unreachable is never delivered. The box keys each delivery on its whole content, which includes the moment the piece read it (timestamp), so a delivery Supabase sent twice would start two runs.
Caution
a workflow that inserts a row into the watched table starts itself again, since the new row is an insert like any other. Guard inside the workflow body with isOwnEcho: call create_row with return_row: true and echoPath: '0.id', then open the workflow with if (await isOwnEcho('supabase', event.record.id)) return;. A write to another table, or an update to this one, starts nothing. A mapping cannot skip an event: every delivery starts a run.
The event
| Field | Type | Always there |
|---|---|---|
type |
string, always INSERT |
yes |
table |
string | yes |
schema |
string | yes |
record |
object, the row's columns as inserted | yes |
old_record |
null | yes |
timestamp |
string, an ISO 8601 time: when the piece read the delivery | yes |
raw_payload |
object, the body Supabase sent | yes |
What's next
- Integrations: connect a provider, and call it from your code.
- Workflows: what a trigger starts, and how a run survives a restart.