ScheduleView on GitHub
Outlook to CRM Sync
Extracts the people behind each day's email conversations and upserts them into Attio with a context note.
integrationmapaiswitch
Source
/**
* Consolidates email conversations into the CRM. Every day it reads the
* last 24 hours of Outlook mail, extracts the people behind each business
* conversation with AI, upserts them into Attio, and files a note on the
* record so the whole team sees the thread context.
*
* Setup:
* 1. swirls add microsoft list_messages
* 2. swirls add attio assert_record create_note
* 3. Deploy, then authorize both connections in Cloud -> Connections.
*/
connection client_microsoft {
label: "Client Microsoft 365"
provider: microsoft
}
connection team_attio {
label: "Attio CRM"
provider: attio
}
schedule nightly_sync {
label: "Nightly CRM Sync"
cron: "0 23 * * *"
}
workflow sync_mail_to_crm {
label: "Sync Mail to CRM"
description: "Extract contacts from the day's email and upsert them into Attio with a context note."
root {
type: code
label: "Build window"
code: @ts {
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
return { since: since }
}
outputSchema: @json {
{
"type": "object",
"properties": { "since": { "type": "string" } }
}
}
}
node fetch_mail {
type: integration
label: "Fetch recent mail"
connection: client_microsoft
action: microsoft_list_messages
params: @ts {
return {
"$filter": "receivedDateTime ge " + context.nodes.root.output.since,
"$orderby": "receivedDateTime desc",
"$top": 50
}
}
}
node sync_each {
type: map
label: "Upsert each contact"
items: @ts { return context.nodes.fetch_mail.output.value || [] }
maxItems: 50
subgraph {
root {
type: code
label: "Extract sender"
inputSchema: @json {
{
"type": "object",
"properties": {
"subject": { "type": "string" },
"bodyPreview": { "type": "string" }
}
}
}
code: @ts {
const m = context.iteration.item
const addr = (m.from && m.from.emailAddress) || {}
return {
email: addr.address || "",
name: addr.name || "",
subject: m.subject || "(no subject)",
preview: m.bodyPreview || ""
}
}
outputSchema: @json {
{
"type": "object",
"properties": {
"email": { "type": "string" },
"name": { "type": "string" },
"subject": { "type": "string" },
"preview": { "type": "string" }
}
}
}
}
node assess {
type: ai
label: "Assess sender"
kind: object
model: "google/gemini-2.5-flash"
prompt: @ts {
const m = context.nodes.root.output
return "Decide whether this email is a real business conversation worth tracking in a CRM. Automated senders, newsletters, and internal system mail are not.\n\nFrom: " + m.name + " <" + m.email + ">\nSubject: " + m.subject + "\nPreview: " + m.preview + "\n\nIf it is worth tracking, write a one-sentence note describing what the conversation is about."
}
schema: @json {
{
"type": "object",
"required": ["track", "note"],
"properties": {
"track": { "type": "boolean" },
"note": { "type": "string" }
}
}
}
}
node route {
type: switch
label: "Track or skip"
cases: ["track", "skip"]
router: @ts {
const m = context.nodes.root.output
if (!m.email) return "skip"
if (!context.nodes.assess.output.track) return "skip"
return "track"
}
}
node upsert_person {
type: integration
label: "Upsert person in Attio"
connection: team_attio
action: attio_assert_record
params: @ts {
const m = context.nodes.root.output
const values = { email_addresses: [m.email] }
if (m.name) values.name = [{ full_name: m.name }]
return {
object: "people",
matching_attribute: "email_addresses",
data: { values: values }
}
}
}
node file_note {
type: integration
label: "File context note"
connection: team_attio
action: attio_create_note
params: @ts {
const m = context.nodes.root.output
const recordId = context.nodes.upsert_person.output.data.id.record_id
return {
data: {
parent_object: "people",
parent_record_id: recordId,
title: m.subject,
format: "plaintext",
content: context.nodes.assess.output.note
}
}
}
}
node synced {
type: code
label: "Mark synced"
code: @ts {
return { email: context.nodes.root.output.email, synced: true }
}
}
node skipped {
type: code
label: "Mark skipped"
code: @ts {
return { email: context.nodes.root.output.email, synced: false }
}
}
flow {
root -> assess
assess -> route
route -["track"]-> upsert_person
upsert_person -> file_note
file_note -> synced
route -["skip"]-> skipped
}
}
}
node summarize {
type: code
label: "Summarize run"
code: @ts {
const rows = context.nodes.sync_each.output || []
const synced = rows.filter(r => r.synced && r.synced.synced).length
const skipped = rows.filter(r => r.skipped).length
return { synced: synced, skipped: skipped }
}
}
flow {
root -> fetch_mail
fetch_mail -> sync_each
sync_each -> summarize
}
}
trigger on_nightly_sync {
schedule:nightly_sync -> sync_mail_to_crm
enabled: true
}
Flow
Trigger to workflow
Workflow nodes