After conducting property inspections, letting agents face hours of email composition: summarizing inspection findings, describing maintenance issues tactfully, explaining access problems or compliance concerns, and maintaining appropriate professional tone across hundreds of landlord communications. This manual composition creates workflow bottlenecks—inspections complete quickly, but reports sit waiting for staff to write accompanying emails.
Week 42 implemented AI-powered email generation automatically composing landlord communications from structured inspection data. The system analyses inspection observations, maintenance issues, photos, and context to generate appropriate emails: positive summaries for clean inspections, maintenance alerts highlighting repairs, compliance notifications for safety concerns. This article explores how we integrated AI generation with background job processing to automate landlord communication whilst maintaining quality and oversight.
What Your Team Will Notice
Each completed inspection now has a "Generate Landlord Email" button. Click it, and the system analyses the inspection, then generates an appropriately-toned email in seconds. The generated email appears in an editable text area—staff review the content, make any necessary adjustments, and send or save the email.
Generated emails adapt to inspection findings: inspections finding no issues produce positive confirmations ("pleased to report the property remains in excellent condition"), inspections documenting maintenance issues list specific concerns with suggested actions, inspections noting compliance problems use appropriately serious language highlighting required remediation.
The AI understands inspection context: routine inspections receive standard summaries, move-out inspections discuss condition relative to inventory, compliance visits focus on regulatory requirements. This contextual awareness produces appropriate communications without staff manually selecting email templates or tone guidance.
Background processing prevents AI generation from blocking the interface. After clicking "Generate", a progress indicator appears whilst the email generates. Staff can navigate away and return later—the generated email persists, ready for review whenever convenient. This asynchronous pattern accommodates AI API latency (sometimes several seconds for complex inspections) without degrading user experience.
Under the Bonnet: AI Email Composer
The email generation service coordinates AI interaction:
# app/services/inspection_email_composer.rb
class InspectionEmailComposer
def initialize(inspection)
@inspection = inspection
@property = inspection.property
@landlord = @property.primary_landlord
end
def generate_email
prompt = build_prompt
response = call_ai_api(prompt)
{
subject: extract_subject(response),
body: extract_body(response),
generated_at: Time.current
}
end
private
def build_prompt
<<~PROMPT
Generate a professional email to a landlord summarizing a property inspection.
Context:
- Property: #{@property.display_address}
- Inspection Type: #{@inspection.inspection_type.titleize}
- Inspection Date: #{@inspection.inspection_date.strftime('%d %B %Y')}
- Landlord: #{@landlord.salutation}
Inspection Findings:
#{format_observations}
#{format_maintenance_issues}
#{format_photos}
Instructions:
- Write in professional British English
- Use appropriate tone for findings (positive for good condition, concerned but constructive for issues)
- Be specific about maintenance issues requiring attention
- Suggest next steps where appropriate
- Keep email concise (under 400 words)
- Format as: Subject line, then email body
Format:
Subject: [subject line]
[email body]
PROMPT
end
def format_observations
return "No observations recorded." unless @inspection.room_observations.present?
observations = @inspection.room_observations.map do |room, notes|
"#{room.titleize}: #{notes}"
end.join("\n")
"Room Observations:\n#{observations}"
end
def format_maintenance_issues
return "" unless @inspection.maintenance_issues.any?
issues = @inspection.maintenance_issues.map do |issue|
priority = issue.priority.present? ? " (#{issue.priority.upcase} priority)" : ""
"- #{issue.title}#{priority}: #{issue.description}"
end.join("\n")
"\nMaintenance Issues Identified:\n#{issues}"
end
def format_photos
photo_count = @inspection.inspection_photos.count
return "" if photo_count.zero?
"\nPhotos: #{photo_count} photos attached to the inspection report"
end
def call_ai_api(prompt)
# Integration with AI API (Claude, GPT, etc.)
client = Anthropic::Client.new(api_key: ENV['ANTHROPIC_API_KEY'])
response = client.messages(
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1000,
messages: [
{
role: 'user',
content: prompt
}
]
)
response.dig('content', 0, 'text')
end
def extract_subject(response)
# Extract subject line from response
if response =~ /Subject:\s*(.+?)$/m
Regexp.last_match(1).strip
else
"Property Inspection Report - #{@property.display_address}"
end
end
def extract_body(response)
# Extract body, removing subject line if present
body = response.sub(/Subject:\s*.+?$/m, '').strip
# Clean up formatting
body.gsub(/\n{3,}/, "\n\n")
end
end
This service structures inspection data into a comprehensive prompt, calls the AI API, and parses the response into subject and body components.
Prompt Engineering for Quality
The prompt design balances specificity with flexibility:
Context Provision: Property address, landlord name, inspection type, and date give the AI situational awareness for appropriate tone and content.
Structured Data: Room observations, maintenance issues, and photo counts provide factual basis preventing hallucination—the AI summarizes actual inspection data rather than inventing details.
Style Instructions: "Professional British English", "concise (under 400 words)", and tone guidance ensure generated emails match agency communication standards.
Output Format: Specifying "Subject: [subject line]" followed by body enables reliable parsing separating these components from AI output.
Background Job Processing
Email generation runs asynchronously to prevent UI blocking:
# app/jobs/generate_inspection_email_job.rb
class GenerateInspectionEmailJob < ApplicationJob
queue_as :default
retry_on Anthropic::NetworkError, wait: :exponentially_longer, attempts: 3
discard_on Anthropic::InvalidRequestError
def perform(inspection_id)
inspection = Inspection.find(inspection_id)
composer = InspectionEmailComposer.new(inspection)
email_content = composer.generate_email
# Store generated email
inspection.update!(
generated_email_subject: email_content[:subject],
generated_email_body: email_content[:body],
email_generated_at: email_content[:generated_at],
email_generation_status: 'completed'
)
# Broadcast completion to frontend via ActionCable
InspectionChannel.broadcast_to(
inspection,
{
event: 'email_generated',
subject: email_content[:subject],
body: email_content[:body]
}
)
rescue StandardError => e
# Mark generation as failed
inspection.update!(
email_generation_status: 'failed',
email_generation_error: e.message
)
# Notify frontend of failure
InspectionChannel.broadcast_to(
inspection,
{
event: 'email_generation_failed',
error: e.message
}
)
raise
end
end
This job handles AI API calls asynchronously, retries transient failures, stores results in the database, and broadcasts updates to the frontend via WebSocket for real-time UI updates.
Real-Time UI Updates with ActionCable
ActionCable provides live updates when generation completes:
// app/javascript/channels/inspection_channel.js
import consumer from "./consumer"
export default class InspectionChannel {
constructor(inspectionId) {
this.inspectionId = inspectionId;
this.subscription = null;
}
connect(callbacks) {
this.subscription = consumer.subscriptions.create(
{
channel: "InspectionChannel",
inspection_id: this.inspectionId
},
{
received(data) {
switch(data.event) {
case 'email_generated':
callbacks.onGenerated(data);
break;
case 'email_generation_failed':
callbacks.onFailed(data);
break;
}
}
}
);
}
disconnect() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
// Usage in Alpine.js component
function inspectionEmail() {
return {
generating: false,
subject: '',
body: '',
error: null,
channel: null,
init() {
this.channel = new InspectionChannel(this.inspectionId);
this.channel.connect({
onGenerated: (data) => {
this.generating = false;
this.subject = data.subject;
this.body = data.body;
},
onFailed: (data) => {
this.generating = false;
this.error = data.error;
}
});
},
async generateEmail() {
this.generating = true;
this.error = null;
// Trigger background job
await fetch(`/inspections/${this.inspectionId}/generate_email`, {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
}
});
// Job will broadcast results via ActionCable
}
};
}
This pattern allows staff to trigger generation, navigate away, and return to find the completed email without polling or page refreshes.
Controller Integration
The controller action triggers email generation:
class InspectionsController < ApplicationController
def generate_email
@inspection = Inspection.find(params[:id])
# Mark as generating
@inspection.update!(email_generation_status: 'generating')
# Queue background job
GenerateInspectionEmailJob.perform_later(@inspection.id)
head :accepted # HTTP 202 - request accepted, processing asynchronously
end
def send_landlord_email
@inspection = Inspection.find(params[:id])
# Send email with generated (and potentially edited) content
InspectionMailer.landlord_report(
@inspection,
subject: params[:subject],
body: params[:body]
).deliver_later
redirect_to @inspection, notice: 'Email sent to landlord'
end
end
The separation between generation and sending allows staff to review and edit generated content before sending.
Testing AI Integration
Testing AI services requires mocking external API calls:
RSpec.describe InspectionEmailComposer do
let(:inspection) { create(:inspection, :with_maintenance_issues) }
let(:composer) { described_class.new(inspection) }
before do
allow_any_instance_of(InspectionEmailComposer)
.to receive(:call_ai_api)
.and_return(mock_ai_response)
end
let(:mock_ai_response) do
<<~EMAIL
Subject: Property Inspection Report - 123 High Street
Dear Mr Smith,
Following our routine inspection of 123 High Street on 15 October 2025,
I'm writing to update you on the property's condition.
Overall, the property is well-maintained. However, we identified one
maintenance issue requiring attention:
- Kitchen tap dripping (MEDIUM priority): The kitchen tap has developed
a persistent drip. We recommend arranging for a plumber to replace
the washer to prevent water wastage.
The inspection report with photos is attached for your reference.
Please let us know if you'd like us to arrange the repair, or if you
prefer to instruct your own contractor.
Best regards,
Agency Staff
EMAIL
end
it "generates appropriate email from inspection data" do
email = composer.generate_email
expect(email[:subject]).to include('Property Inspection Report')
expect(email[:body]).to include('routine inspection')
expect(email[:body]).to include('Kitchen tap dripping')
end
it "includes landlord salutation" do
email = composer.generate_email
expect(email[:body]).to include(inspection.property.primary_landlord.salutation)
end
end
These tests verify generation logic without making actual AI API calls during test runs.
What's Next
The AI email generation foundation enables sophisticated features: email templates with variable tone (formal, friendly, urgent), multilingual generation supporting international landlords, learning from staff edits improving future generations, and bulk email generation for portfolio-wide inspection campaigns.
Future enhancements might include sentiment analysis ensuring appropriate emotional tone, compliance checking flagging emails missing required regulatory language, personalization using landlord communication history, and A/B testing different AI prompts optimizing email effectiveness.
By implementing AI-powered email generation with background processing, LetAdmin transforms hours of manual communication composition into minutes of review and refinement, allowing agencies to scale landlord communication without proportionally scaling staff writing time.
