# How-to guides - Hybrid polling + webhook triggers
Webhooks open up a wide range of use cases that are time sensitive in nature, such as receiving the event when a customer makes a purchase. There are, however, scenarios where webhook events can be lost. This could be due to transient network errors that any HTTP requests can be susceptible to, or simply when the recipe is stopped for a short period of time and webhooks were sent during that period.
Hybrid polling + webhook triggers give you the best of both worlds, where your triggers can function in real-time to produce jobs but also provide you with the additional guarantee of picking up events even if the webhook was lost.
Before continuing
The guide below builds on top of knowledge for both webhook and polling triggers. It is recommended that you read through the guides for those respective triggers first.
# Why implement Hybrid triggers
Hybrid triggers present numerous benefits over traditional webhook triggers through three key improvements:
- Webhook events trigger a signal to Workato to pull new records from the target system. This lowers potential for spoofed webhooks from creating bad data and also ensures that potentially lost webhooks will still be retrieve when a new webhook is eventually received.
- Allow your trigger output for each job to have all data needed, instead of a skinny payload that is often sent in the webhook.
- Prevent job loss when a recipe is stopped for a period of time. When the recipe is started again, Workato executes a poll immediately to retrieve all records.
# Mechanics of Hybrid triggers
Hybrid trigger combine important lambdas from webhook triggers and polling triggers respectively. For static webhook hybrid triggers, you will need the webhook_keys
and webhook_key
lambdas, alongside the poll
lambda. For dynamic webhook hybrid triggers, you will need the webhook_subscribe
and webhook_unsubscribe
lambdas, alongside the poll
lambda.
When using hybrid triggers, the mechanics for pulling events change in 2 significant ways:
- When a webhook event is received, the
poll
lambda is invoked to get new events based on the closure from the previous poll. - When a recipe is started/restarted, the
poll
lambda is invoked to get new events based on the closure from the previous poll.
TIP
In the case of a burst of webhook events to a trigger, Workato's engine throttles polls intelligently to pull all records without having poll the API for each webhook event. This prevents rate limits from being hit from busy webhooks.
For example, when a burst of 100 webhook events are received over 1 minute, you can expect to receive 4-5 polls with the last poll being later than the time when the last webhook event was received.
Workato also executes the poll
lambda every 12 hours to ensure that in case of webhook outages, records are still picked up with some delay.
# Sample connector - Chargebee
{
title: 'My Chargebee connector',
webhook_keys: lambda do |params, headers, payload|
# Chargebee events come in the form "subscription_changed", "subscription_renewed", "customer_changed" etc
# Use .split to get the main object
"#{payload['event_type']}".split("_").first
end,
# More connector code here
triggers: {
new_updated_subscription: {
title: 'New/updated subscription',
subtitle: "Triggers when a subscription is " \
"created/updated in Chargebee",
description: lambda do |input, picklist_label|
"New/updated <span class='provider'>subscription</span> in " \
"<span class='provider'>Chargebee</span>"
end,
help: lambda do |input, picklist_label, connection, webhook_base_url|
next unless webhook_base_url.present?
<<~HTML
Creates a job immediately when a subscription is created/updated from Chargebee. To set this webhook up,
you will need to register the webhook below in Chargebee under "settings" => "webhooks" => "new" and provide the events for subscription_changed and subscription_created. <br>
<b>Webhook endpoint URL</b>
<b class="tips__highlight">#{webhook_base_url}</b>
HTML
end,
input_fields: lambda do
[
{
name: 'since',
type: :timestamp,
optional: true
}
]
end,
webhook_key: lambda do |connection, input|
"subscription"
end,
poll: lambda do |connection, input, closure|
page_size = 100
closure = {} unless closure.present?
closure['updated_since'] = (closure['updated_since'] || input['since'] || 1.hours.ago).to_time.utc.to_i
params = {
"sort_by[asc]": 'updated_at',
limit: page_size,
"updated_at[after]": closure['updated_since']
}
params['offset'] = closure['offset']
response = get("/api/v2/subscriptions", params)
if response['next_offset'].present?
closure['offset'] = response['next_offset']
else
closure['offset'] = nil
closure['updated_since'] = response['list'].last[input['object']]['updated_at'] unless response['list'].size == 0
end
{
events: response['list'],
next_poll: closure,
can_poll_more: response['next_offset'].present?
}
end,
dedup: lambda do |record|
"#{record['subscription']['id']}@#{record['subscription']['updated_at']}"
end,
output_fields: lambda do |object_definitions|
object_definitions['subscription']
end
}
},
# More connector code here
}
# Step 1 - Implement your chosen webhook trigger
Depending on what the app you are building the connector for, build the appropriate webhook trigger type. Dynamic webhooks are always preferred as they minimize the amount of setup needed by the end user. In our example, Chargebee requires you to manually setup webhook subscriptions so we have used a static webhook trigger. Refer more to our guides on each of the types of webhooks to learn more.
For dynamic webhook triggers, you are expected to define the webhook_subscribe
and webhook_unsubscribe
lambda.
For static webhook triggers, you are expected to define the webhook_keys
and webhook_key
lambda.
# Step 2 - Define the poll block
Instead of defining the webhook_notification
lambda, building a hybrid trigger requires that you define the poll
lambda instead. The poll
lambda should function similar to any other polling trigger, whereby it needs an API endpoint to pull new records. Refer to our polling trigger guides to understand more.
poll: lambda do
page_size = 100
closure = {} unless closure.present?
closure['updated_since'] = (closure['updated_since'] || input['since'] || 1.hours.ago).to_time.utc.to_i
params = {
"sort_by[asc]": 'updated_at',
limit: page_size,
"updated_at[after]": closure['updated_since']
}
params['offset'] = closure['offset']
response = get("/api/v2/subscriptions", params)
if response['next_offset'].present?
closure['offset'] = response['next_offset']
else
closure['offset'] = nil
closure['updated_since'] = response['list'].last[input['object']]['updated_at'] unless response['list'].size == 0
end
{
events: response['list'],
next_poll: closure,
can_poll_more: response['next_offset'].present?
}
end,
In our example, we simply query Chargebee's subscription API to give us the subscriptions created/updated after the last time we polled.
# Step 4 - Defining output fields and dedup
When defining the output fields and dedup, take note that this should be based on the poll
lambda's output and not the actual webhook payload. Essentially, the webhook payload is discarded in favor of the records received in the poll
lambda.
dedup: lambda do |record|
"#{record['subscription']['id']}@#{record['subscription']['updated_at']}"
end,
output_fields: lambda do |object_definitions|
object_definitions['subscription']
end
# Rate limits
This trigger is subject to our webhook gateway's limits.
Last updated: 4/5/2023, 11:28:53 AM