Skip to main content

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.