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
nightly_syncschedule
sync_mail_to_crm
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
Workflow nodes
CODE
Code Node
Build window
INTEGRATION
Integration Node
Fetch recent mail
MAP
Map Node
Upsert each contact
CODE
Code Node
Summarize run
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.