Automation Script Examples
This section provides complete examples of Automation Scripts to help you understand how to build your own workflows.
Gmail Intent Classifier
This example demonstrates a complete automation script that processes Gmail messages, classifies their intent using OpenAI, and sends results to Slack.
class GmailIntentClassifier < BaseAutomationService
def execute
Rails.logger.info("Starting Gmail intent classification process")
@errors = ActiveModel::Errors.new(self)
# Initialize clients
initialize_clients
# Setup time range from variables or trigger payload
setup_time_range
# Process messages
process_messages
rescue StandardError => e
Rails.logger.error("Automation workflow script error: #{e.message} [Account: #{@automation_workflow_execution.account.name}]")
raise e
end
private
def initialize_clients
@gmail_client = CommerceAutomationNodes::GmailNode.new(credentials: @credentials)
@openai_client = CommerceAutomationNodes::OpenaiNode.new(credentials: @credentials)
@slack_client = CommerceAutomationNodes::SlackNode.new(credentials: @credentials)
Rails.logger.info("Clients initialized successfully")
end
def setup_time_range
time_zone = @variables.dig("time_zone") || "America/New_York"
@start_time = if @variables.dig("start_time")
Time.parse(@variables.dig("start_time")).in_time_zone(time_zone)&.to_i
elsif @automation_workflow_execution.automation_workflow.last_successful_execution_at
@automation_workflow_execution.automation_workflow.last_successful_execution_at.in_time_zone(time_zone)&.to_i
else
30.minutes.ago.in_time_zone(time_zone)&.to_i
end
@end_time = if @variables.dig("end_time")
Time.parse(@variables.dig("end_time")).in_time_zone(time_zone)&.to_i
else
Time.now.in_time_zone(time_zone)&.to_i
end
Rails.logger.info("Time range determined: #{@start_time} to #{@end_time}")
end
def process_messages
# Get messages within time range
messages_response = @gmail_client.list_messages(
user_id: @variables.dig("gmail_user_id"),
q: "after:#{@start_time} before:#{@end_time}"
)
messages = messages_response&.parsed_body&.dig("messages") || []
Rails.logger.info("Found #{messages.length} messages to process")
classified_messages = []
messages.each do |message_summary|
begin
message_id = message_summary.dig("id")
# Skip already processed messages
if already_processed?(message_id)
Rails.logger.info("Skipping already processed message: #{message_id}")
next
end
# Get full message details
message = get_full_message(message_id)
# Extract message content
subject, body, from = extract_message_content(message)
# Classify intent using OpenAI
classification = classify_and_critique_intent(subject, body)
classified_messages << build_classification_result(message, classification, from, subject)
# Store the classification result
store_intent_and_example(classification, body, from, message_id)
rescue => e
Rails.logger.error("Error processing message #{message_summary.dig('id')}: #{e.message}")
next
end
end
# Send results to Slack
send_classification_results(classified_messages)
end
def already_processed?(message_id)
ConversationIntentExample.where(
account_id: @automation_workflow_execution.account.id
).where("source_metadata @> ?", {gmail_message_id: message_id&.to_s}.to_json).exists?
end
def get_full_message(message_id)
message_response = @gmail_client.get_message(
user_id: @variables.dig("gmail_user_id"),
message_id: message_id
)
message_response&.parsed_body
end
def extract_message_content(message)
subject = message.dig("payload", "headers")&.find { |h| h["name"] == "Subject" }&.dig("value")
body = extract_message_body(message)
from = message.dig("payload", "headers")&.find { |h| h["name"] == "From" }&.dig("value")
[subject, body, from]
end
def extract_message_body(message)
if message.dig("payload", "parts")
# Multipart message
text_part = message.dig("payload", "parts").find { |part| part.dig("mimeType") == "text/plain" }
html_part = message.dig("payload", "parts").find { |part| part.dig("mimeType") == "text/html" }
body = text_part&.dig("body", "data") || html_part&.dig("body", "data")
body ? Base64.urlsafe_decode64(body) : ""
else
# Single part message
body = message.dig("payload", "body", "data")
body ? Base64.urlsafe_decode64(body) : ""
end
end
def classify_and_critique_intent(subject, body)
# First, get initial classification
initial_classification = classify_intent(subject, body)
# Then critique and refine it
critique_classification(initial_classification, subject, body)
end
def classify_intent(subject, body)
@account = @automation_workflow_execution.account
existing_intents = ConversationIntent.where(account_id: @account.id)
.where(deleted_at: nil)
.pluck(:name, :description)
.map { |name, desc| {name: name, description: desc} }
content = "Subject: #{subject}\n\nBody: #{body}"
prompt = build_classification_prompt(existing_intents, content)
response = @openai_client.send_message(
message: prompt,
model: @variables.dig("openai_model") || "gpt-4"
)
Rails.logger.info("Classification response: #{response}")
begin
response
rescue
default_classification_response
end
end
def critique_classification(initial_classification, subject, body)
existing_intents = ConversationIntent.where(account_id: @account.id)
.where(deleted_at: nil)
.pluck(:name, :description)
.map { |name, desc| {name: name, description: desc} }
content = "Subject: #{subject}\n\nBody: #{body}"
critique_prompt = build_critique_prompt(initial_classification, content, existing_intents)
response = @openai_client.send_message(
message: critique_prompt,
model: @variables.dig("openai_model") || "gpt-4"
)
begin
response
rescue
initial_classification.merge({
"critique_feedback" => "Failed to critique classification"
})
end
end
def build_classification_prompt(existing_intents, content)
<<~PROMPT
You are an expert email intent classifier for a 3PL company.
Existing intents:
#{existing_intents.map { |i| "- #{i[:name]}: #{i[:description]}" }.join("\n")}
Guidelines:
1. Try to match existing intents first
2. Create new intents only if necessary
3. Use specific prefixes: GET_, CREATE_, UPDATE_, DELETE_, INQUIRE_ABOUT_, UNKNOWN
4. Be highly specific in intent names
Email Content:
#{content}
Respond in JSON format:
{
"intent": "SPECIFIC_INTENT_NAME",
"confidence": 0.XX,
"subcategory": "detailed subcategory",
"reasoning": "brief explanation",
"description": "one line description",
"is_new_intent": true/false
}
PROMPT
end
def build_critique_prompt(initial_classification, content, existing_intents)
<<~PROMPT
Review this email intent classification as a Customer Support Manager.
Original Email:
#{content}
Initial Classification:
Intent: #{initial_classification["intent"]}
Confidence: #{initial_classification["confidence"]}
Existing intents:
#{existing_intents.map { |i| "- #{i[:name]}: #{i[:description]}" }.join("\n")}
Provide critique in JSON format:
{
"intent": "FINAL_INTENT_NAME",
"confidence": 0.XX,
"subcategory": "refined subcategory",
"critique_feedback": "explanation of changes",
"description": "refined description",
"is_new_intent": true/false
}
PROMPT
end
def build_classification_result(message, classification, from, subject)
{
message_id: message.dig("id"),
thread_id: message.dig("threadId"),
from: from,
subject: subject,
received_at: Time.at(message.dig("internalDate").to_i/1000).strftime("%Y-%m-%d %H:%M:%S"),
intent: classification.dig("intent"),
confidence: classification.dig("confidence"),
subcategory: classification.dig("subcategory"),
critique_feedback: classification.dig("critique_feedback")
}
end
def store_intent_and_example(classification, body, from_email, gmail_message_id)
intent = ConversationIntent.find_or_create_by(
name: classification["intent"],
account_id: @account.id
) do |i|
i.description = classification["description"]
i.category = classification["intent"].split("_").first
i.confidence_threshold = 0.7
i.is_active = true
i.embedding_model = @variables.dig("openai_model") || "text-embedding-3-small"
end
ConversationIntentExample.create!(
conversation_intent_id: intent.id,
account_id: @account.id,
phrase: body,
embedding_model: @variables.dig("openai_model") || "text-embedding-3-small",
confidence_score: classification["confidence"].to_f,
source_type: "email",
source_metadata: {
from_email: from_email,
subcategory: classification["subcategory"],
critique_feedback: classification["critique_feedback"],
gmail_message_id: gmail_message_id
},
validation_status: "pending"
)
end
def send_classification_results(classified_messages)
grouped_messages = classified_messages.group_by { |m| m[:intent] }
summary = build_summary_text(grouped_messages)
@slack_client.post_message(
channel: @variables.dig("slack_channel"),
text: "Email Intent Classification Results",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "📧 *Email Intent Classification Results*\n" +
"Time range: #{format_time(@start_time)} to #{format_time(@end_time)}\n" +
"Total messages: #{classified_messages.length}\n\n" +
"```#{summary}```"
}
}
]
)
send_response(
response_json: {
time_range: {
start: Time.at(@start_time),
end: Time.at(@end_time)
},
total_messages: classified_messages.length,
classifications: classified_messages,
summary: grouped_messages.transform_values(&:length)
}
)
end
def build_summary_text(grouped_messages)
grouped_messages.map do |intent, messages|
"#{intent&.upcase} (#{messages&.length} messages)\n" +
messages.map do |m|
"- #{m[:subject]} (#{m[:confidence]&.round(2)} confidence)\n #{m[:critique_feedback]}"
end.join("\n")
end.join("\n\n")
end
def format_time(timestamp)
Time.at(timestamp).strftime('%Y-%m-%d %H:%M')
end
def default_classification_response
{
"intent" => "UNKNOWN",
"confidence" => 0,
"subcategory" => "parse_error",
"reasoning" => "Failed to parse response",
"description" => "Failed to parse response",
"is_new_intent" => false
}
end
end
Key Implementation Points
1. Service Integration
The script demonstrates how to integrate multiple services (Gmail, OpenAI, Slack) using the Commerce Automation Nodes.
2. Error Handling
Each major operation is wrapped in error handling to ensure the script continues processing even if individual items fail.
3. Data Persistence
The script stores classification results in the database and checks for already processed messages to avoid duplication.
4. Configurable Parameters
The script uses @variables to access configuration parameters like time zones, model names, and API endpoints.
5. Comprehensive Logging
Throughout the script, detailed logging helps with debugging and monitoring the automation process.
6. Response Formatting
The script formats and sends results both to Slack for immediate notification and returns structured data for further processing.
Best Practices Demonstrated
- Modular Design: Breaking complex operations into smaller, focused methods
- Safe Navigation: Using safe navigation (
&.dig) when accessing nested data - Time Handling: Proper timezone handling and time range calculations
- JSON Processing: Safe JSON parsing with fallback error handling
- Database Operations: Efficient querying and data persistence
- External API Integration: Proper API client usage with error handling
This example provides a comprehensive template for building your own automation scripts while following Ruby and Rails best practices.