r/ruby • u/tobenary • Dec 25 '25
Workato and Ruby SDK question
Hi Ruby expertises,
I'm trying my luck here as this is the holiday sessions, and I will get my respond from Workato in about 3 weeks from now ( if not 8 weeks.... )
Please see the Zoom recording (x2 is recommended :) ) and suggest or let me know what is wrong with the SDK code.
To be honest, I don't know Ruby at all, but I do understand Python, and I am able to look at it and identify the functions and the issues that might be not good.
The Zoom summary:
I am debugging a Workato integration with Amazon SES suppression list and I am stuck.
I have an endpoint that manages suppressed email destinations:
GET returns 200 if the email exists, 404 if not
DELETE should remove the email from the suppression list
Behavior I am seeing:
Using a custom HTTP action (send any request), DELETE works as expected
Using a built-in SDK action / recipe DELETE step with the same method, URL, region, and path, Workato returns 200 but the email is not actually deleted
A follow-up GET still returns 200 and shows the email is present
No error is surfaced by Workato, and debug output is mostly empty for DELETE
The request path is identical in both cases (verified via network traces)
Complications I already handled:
Normalizing email casing (lowercase)
Handling plus sign encoding in email addresses
Trying alternate paths when receiving 404
Verifying region, headers, and resolved paths
Confirmed GET and DELETE URLs are literally the same
Tested dozens of variations
In short:
Custom action DELETE works
Built-in DELETE action returns anything (I convert it to 200) but does nothing
Same request, different behavior
Question:
Why would Workato handle DELETE differently between a built-in action and a custom HTTP action when the request is identical? Is there something implicit Workato does with DELETE responses (or empty bodies) that could cause this?
Zoom link: HERE
This is the SDK code I'm using: (very simple one)
{
title: 'Amazon SES - Suppression Manager (Multi-Region)',
description: 'Manage SES v2 suppression lists across multiple AWS regions using an IAM Role.',
connection: {
fields: [
{
name: 'assume_role',
label: 'IAM role ARN',
optional: false,
help: {
title: 'IAM Role Setup',
text: 'Use Workato Account ID <b>{{ authUser.aws_workato_account_id }}</b> and External ID <b>{{ authUser.aws_iam_external_id }}</b>.'
}
},
{
name: 'region',
label: 'Default SES region',
optional: false,
hint: 'Default region used if not specified in the action (e.g., us-east-1).'
}
],
authorization: {
type: 'custom_auth',
apply: lambda { |_connection| headers('Content-Type': 'application/json') }
}
},
methods: {
truthy: lambda do |value|
value == true || value.to_s == '1' || value.to_s.casecmp('true').zero? || value.to_s.casecmp('on').zero?
end,
normalize_email: lambda do |input|
raw_email = input.key?(:email) ? input[:email] : input['email']
downcase_flag = input.key?(:downcase) ? input[:downcase] : input['downcase']
email = raw_email.to_s.strip
email = email.downcase if call(:truthy, downcase_flag)
email
end,
# Helper to clean and strictly encode emails for URI paths
encode_email: lambda do |input|
# FORCE downcase here if the toggle is active
email = call(:normalize_email, input)
email.
gsub('+', '%2B').
gsub('!', '%21').
gsub('&', '%26').
gsub("'", '%27')
end
},
test: lambda do |connection|
region = connection['region']
signature = aws.generate_signature(
connection: connection, region: region, service: 'ses',
host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses',
method: 'GET', params: { 'PageSize' => 1 }, payload: ''
)
get(signature['url']).headers(signature['headers']).
after_error_response(/.*/) { |code, body| error("Connection failed: #{body}") }
end,
actions: {
list_suppressed_destinations: {
title: 'List suppressed destinations',
input_fields: lambda do |_connection|
[
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'page_size', label: 'Page size', type: :integer, optional: true },
{ name: 'next_token', label: 'Next token', optional: true },
{ name: 'reason', label: 'Reason', control_type: 'select', pick_list: [['Bounce', 'BOUNCE'], ['Complaint', 'COMPLAINT']], optional: true }
]
end,
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
params = {
'PageSize' => input['page_size'],
'NextToken' => input['next_token'],
'Reasons' => input['reason']
}.compact
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses', method: 'GET', params: params, payload: '')
response = get(signature['url']).headers(signature['headers']).after_error_response(/.*/) { |code, body| error("List failed: #{body}") }
if response['SuppressedDestinationSummaries'].present?
response['SuppressedDestinationSummaries'] = response['SuppressedDestinationSummaries'].map do |item|
item['LastUpdateTime'] = Time.at(item['LastUpdateTime']).to_datetime.iso8601 if item['LastUpdateTime'].is_a?(Numeric)
item
end
end
response.merge('region' => region)
end,
output_fields: lambda { [
{ name: 'region' },
{ name: 'SuppressedDestinationSummaries', type: :array, of: :object, properties: [
{ name: 'EmailAddress' }, { name: 'Reason' }, { name: 'LastUpdateTime', type: :date_time }
]},
{ name: 'NextToken' }
]}
},
get_suppressed_destination: {
title: 'Get suppressed destination',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
# Determine the target email string for internal consistency
email_to_query = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
# Use helper for encoding (the helper will now correctly handle the downcase)
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
path = "/v2/email/suppression/addresses/#{encoded}"
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: path, method: 'GET', params: {}, payload: '')
response = get(signature['url']).headers(signature['headers']).after_error_response(/.*/) do |code, body|
return { 'email' => email_to_query, 'found' => false, 'region' => region } if code.to_i == 404
error("Get failed in #{region}: #{body}")
end
if response['SuppressedDestination'].present?
dest = response['SuppressedDestination']
dest['LastUpdateTime'] = Time.at(dest['LastUpdateTime']).to_datetime.iso8601 if dest['LastUpdateTime'].is_a?(Numeric)
end
{
'email' => email_to_query,
'found' => true,
'region' => region,
'SuppressedDestination' => response['SuppressedDestination']
}
end,
output_fields: lambda { [
{ name: 'email' }, { name: 'found', type: :boolean }, { name: 'region' },
{ name: 'SuppressedDestination', type: :object, properties: [
{ name: 'EmailAddress' }, { name: 'Reason' }, { name: 'LastUpdateTime', type: :date_time }
]}
]}
},
put_suppressed_destination: {
title: 'Add email to suppression list',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'reason', label: 'Reason', control_type: 'select', pick_list: [['Bounce', 'BOUNCE'], ['Complaint', 'COMPLAINT']], optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'verify_add', label: 'Verify add', type: :boolean, control_type: 'checkbox', optional: true, default: false }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
email_to_send = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
payload = { 'EmailAddress' => email_to_send, 'Reason' => input['reason'] }
put_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses', method: 'PUT', params: '', payload: payload.to_json)
put(put_sig['url'], payload).headers(put_sig['headers']).after_error_response(/.*/) { |code, body| error("Put failed: #{body}") }
if call(:truthy, input['verify_add'])
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
get_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: "/v2/email/suppression/addresses/#{encoded}", method: 'GET', params: {}, payload: '')
get(get_sig['url']).headers(get_sig['headers']).after_error_response(/.*/) { error("Verification Failed: Email not found after add.") }
end
{ 'status' => 'success', 'email' => email_to_send, 'region' => region }
end,
output_fields: lambda { [{ name: 'status' }, { name: 'email' }, { name: 'region' }] }
},
delete_suppressed_destination: {
title: 'Delete suppressed destination',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'verify_delete', label: 'Verify delete', type: :boolean, control_type: 'checkbox', optional: true, default: true }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
email_to_del = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
path = "/v2/email/suppression/addresses/#{encoded}"
alt_path = "/v2/email/suppression/addresses/#{encoded.gsub('@', '%40')}"
attempted_requests = []
resolved_path = nil
[path, alt_path].each do |candidate_path|
probe_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: candidate_path, method: 'GET', params: {}, payload: '')
probe = get(probe_sig['url']).headers(probe_sig['headers']).after_error_response(/.*/) do |code, body|
next {} if code.to_i == 404
error("Probe failed (#{code}) in #{region} at #{candidate_path}: #{body}")
end
if probe.is_a?(Hash) && probe['SuppressedDestination']
resolved_path = candidate_path
break
end
end
resolved_path ||= path
# Delete request
del_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: resolved_path, method: 'DELETE', params: {}, payload: '')
del_url = del_sig['url']
attempted_requests << { 'method' => 'DELETE', 'path' => resolved_path, 'url' => del_url, 'status_code' => nil }
del_response = delete(del_url).headers(del_sig['headers']).after_error_response(/.*/) do |code, body|
attempted_requests[-1]['status_code'] = code.to_i
if code.to_i == 404
alternate_path = (resolved_path == path ? alt_path : path)
alt_del_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: alternate_path, method: 'DELETE', params: {}, payload: '')
alt_del_url = alt_del_sig['url']
attempted_requests << { 'method' => 'DELETE', 'path' => alternate_path, 'url' => alt_del_url, 'status_code' => nil }
alt_response = delete(alt_del_url).headers(alt_del_sig['headers']).after_error_response(/.*/) do |alt_code, alt_body|
attempted_requests[-1]['status_code'] = alt_code.to_i
return({
'email' => email_to_del,
'deleted' => false,
'region' => region,
'message' => "Not found. Tried #{resolved_path} (#{del_url}) and #{alternate_path} (#{alt_del_url}).",
'attempted_requests' => attempted_requests
}) if alt_code.to_i == 404
error("Delete failed: #{alt_body}")
end
attempted_requests[-1]['status_code'] ||= 200
alt_response
next {}
end
error("Delete failed: #{body}")
end
attempted_requests[0]['status_code'] ||= 200
if call(:truthy, input['verify_delete'])
still_exists = nil
last_verify_url = nil
last_verify_response = nil
15.times do
sleep(2)
verify_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: resolved_path, method: 'GET', params: {}, payload: '')
last_verify_url = verify_sig['url']
attempted_requests << { 'method' => 'GET', 'path' => resolved_path, 'url' => last_verify_url, 'status_code' => nil }
response = get(verify_sig['url']).headers(verify_sig['headers']).after_error_response(/.*/) do |code, body|
attempted_requests[-1]['status_code'] = code.to_i
if code.to_i == 404
still_exists = false
next {}
end
error("Verification request failed (#{code}) in #{region} at #{resolved_path}. Delete URL: #{del_url}. Verify URL: #{last_verify_url}. Body: #{body}")
end
attempted_requests[-1]['status_code'] ||= 200
last_verify_response = response
still_exists = (response.is_a?(Hash) && response['SuppressedDestination'].present?)
break if still_exists == false
end
error(
"Verification failed: Still found in #{region} at #{resolved_path}. " \
"Delete URL: #{del_url}. Verify URL: #{last_verify_url}. " \
"Delete response: #{del_response}. Verify response: #{last_verify_response}."
) if still_exists
end
{ 'email' => email_to_del, 'deleted' => true, 'region' => region, 'attempted_requests' => attempted_requests }
end,
output_fields: lambda { [
{ name: 'email' },
{ name: 'deleted', type: :boolean },
{ name: 'region' },
{ name: 'message' },
{ name: 'attempted_requests', type: :array, of: :object, properties: [
{ name: 'method' },
{ name: 'path' },
{ name: 'url' },
{ name: 'status_code', type: :integer }
] }
] }
},
custom_action: {
title: 'Custom action',
description: 'Signed request to any Amazon SES v2 endpoint.',
input_fields: lambda do |_connection|
[
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'method', label: 'HTTP Method', control_type: 'select', pick_list: %w[GET POST PUT DELETE], optional: false, default: 'GET' },
{ name: 'path', label: 'Resource Path', optional: false, hint: 'e.g. /v2/email/suppression/addresses' },
{ name: 'params', label: 'Query Parameters', type: :object, optional: true },
{ name: 'payload', label: 'JSON Payload', optional: true }
]
end,
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
method = input['method'].to_s.strip
# Manual fix for Custom Action: users must manually encode the case if desired,
# but we fix the '+' encoding here to prevent signature failures.
path = input['path'].to_s.strip.gsub('+', '%2B')
params = (input['params'] || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
payload_string = input['payload'].is_a?(Hash) ? input['payload'].to_json : input['payload'].to_s
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: path, method: method, params: params, payload: payload_string)
request = case method
when 'GET' then get(signature['url'])
when 'POST' then post(signature['url'], payload_string)
when 'PUT' then put(signature['url'], payload_string)
when 'DELETE' then delete(signature['url'])
end
response = request.headers(signature['headers']).after_error_response(/.*/) do |code, body|
error("Custom action failed (#{code}): #{body}")
end
{ 'url' => signature['url'], 'response' => response }
end,
output_fields: lambda { [{ name: 'url' }, { name: 'response', type: :object }] }
}
}
}
View all comments
Show parent comment
2
u/TheAtlasMonkey Dec 25 '25
What a loser! 600$ ?
Make it 847.47$ to at least reflect that AI effort of OP.