Swirls

SWIRLS_

  • How it works
  • Pricing

56 matches

OverviewDeliver client operations10Win and grow accounts8Resolve customer issues6Create and distribute content9Run people and operations5Review money and agreements3Monitor systems and data9Compose dependable work6

56 matches

OverviewDeliver client operations10Win and grow accounts8Resolve customer issues6Create and distribute content9Run people and operations5Review money and agreements3Monitor systems and data9Compose dependable work6
← Client OperationsScheduleView on GitHub

Outlook Inbox Triage

Classifies unread Outlook mail every morning, archives the noise, and emails a digest of what needs a reply.

integrationmapaiemail

Source

/**
 * Morning inbox triage for a client mailbox. Pulls unread Outlook mail,
 * classifies every message with AI, archives newsletters and junk, and
 * emails a digest of what actually needs a reply.
 *
 * Setup:
 *   1. swirls add microsoft list_messages move_message
 *   2. Deploy, then authorize the Microsoft connection in Cloud -> Connections.
 */

connection client_microsoft {
  label: "Client Microsoft 365"
  provider: microsoft
}

schedule weekday_mornings {
  label: "Weekday Mornings"
  cron: "0 7 * * 1-5"
}

workflow triage_inbox {
  label: "Triage Inbox"
  description: "Classify unread Outlook mail, archive the noise, and send a digest of what needs a reply."

  root {
    type: code
    label: "Configure"
    code: @ts {
      return { maxMessages: 25, digestTo: "[email protected]" }
    }
    outputSchema: @json {
      {
        "type": "object",
        "properties": {
          "maxMessages": { "type": "number" },
          "digestTo": { "type": "string" }
        }
      }
    }
  }

  node fetch_unread {
    type: integration
    label: "Fetch unread mail"
    connection: client_microsoft
    action: microsoft_list_messages
    params: @ts {
      return {
        "$filter": "isRead eq false",
        "$orderby": "receivedDateTime desc",
        "$top": context.nodes.root.output.maxMessages
      }
    }
  }

  node classify_each {
    type: map
    label: "Classify each message"
    items: @ts { return context.nodes.fetch_unread.output.value || [] }
    maxItems: 25

    subgraph {
      root {
        type: code
        label: "Extract fields"
        inputSchema: @json {
          {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "subject": { "type": "string" },
              "bodyPreview": { "type": "string" }
            }
          }
        }
        code: @ts {
          const m = context.iteration.item
          const from = (m.from && m.from.emailAddress && m.from.emailAddress.address) || "unknown"
          return {
            id: m.id,
            subject: m.subject || "(no subject)",
            preview: m.bodyPreview || "",
            from: from
          }
        }
        outputSchema: @json {
          {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "subject": { "type": "string" },
              "preview": { "type": "string" },
              "from": { "type": "string" }
            }
          }
        }
      }

      node classify {
        type: ai
        label: "Classify"
        kind: object
        model: "google/gemini-2.5-flash"
        prompt: @ts {
          const m = context.nodes.root.output
          return "Classify this email for a busy business owner.\n\nFrom: " + m.from + "\nSubject: " + m.subject + "\nPreview: " + m.preview + "\n\nCategories:\n- needs_reply: a person is waiting on an answer\n- fyi: worth seeing, no reply needed\n- noise: newsletter, promotion, or automated notification"
        }
        schema: @json {
          {
            "type": "object",
            "required": ["category", "summary"],
            "properties": {
              "category": { "type": "string", "enum": ["needs_reply", "fyi", "noise"] },
              "summary": { "type": "string" }
            }
          }
        }
      }

      node pack {
        type: code
        label: "Pack result"
        code: @ts {
          const m = context.nodes.root.output
          const c = context.nodes.classify.output
          return {
            id: m.id,
            subject: m.subject,
            from: m.from,
            category: c.category,
            summary: c.summary
          }
        }
      }

      flow {
        root -> classify
        classify -> pack
      }
    }
  }

  node archive_noise {
    type: map
    label: "Archive the noise"
    items: @ts {
      const rows = context.nodes.classify_each.output || []
      return rows.map(r => r.pack).filter(m => m && m.category === "noise")
    }
    maxItems: 25

    subgraph {
      root {
        type: code
        label: "Message id"
        inputSchema: @json {
          { "type": "object", "properties": { "id": { "type": "string" } } }
        }
        code: @ts { return { id: context.iteration.item.id } }
        outputSchema: @json {
          { "type": "object", "properties": { "id": { "type": "string" } } }
        }
      }

      node archive {
        type: integration
        label: "Move to archive"
        connection: client_microsoft
        action: microsoft_move_message
        params: @ts {
          return { message_id: context.nodes.root.output.id, destinationId: "archive" }
        }
      }

      flow {
        root -> archive
      }
    }
  }

  node send_digest {
    type: email
    label: "Send digest"
    from: @ts { return "[email protected]" }
    to: @ts { return context.nodes.root.output.digestTo }
    subject: @ts {
      const rows = (context.nodes.classify_each.output || []).map(r => r.pack).filter(Boolean)
      const needs = rows.filter(m => m.category === "needs_reply")
      return "Inbox triage: " + needs.length + " emails need a reply"
    }
    text: @ts {
      const rows = (context.nodes.classify_each.output || []).map(r => r.pack).filter(Boolean)
      const needs = rows.filter(m => m.category === "needs_reply")
      const fyi = rows.filter(m => m.category === "fyi")
      const noise = rows.filter(m => m.category === "noise")

      const lines = []
      lines.push("Needs a reply (" + needs.length + "):")
      needs.forEach(m => lines.push("- " + m.from + ": " + m.subject + " -- " + m.summary))
      lines.push("")
      lines.push("FYI (" + fyi.length + "):")
      fyi.forEach(m => lines.push("- " + m.from + ": " + m.subject))
      lines.push("")
      lines.push("Archived as noise: " + noise.length + " messages")
      return lines.join("\n")
    }
  }

  flow {
    root -> fetch_unread
    fetch_unread -> classify_each
    classify_each -> archive_noise
    archive_noise -> send_digest
  }
}

trigger on_weekday_morning {
  schedule:weekday_mornings -> triage_inbox
  enabled: true
}

Flow

Trigger to workflow

weekday_morningsschedule
triage_inbox
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
Configure
INTEGRATION
Integration Node
Fetch unread mail
MAP
Map Node
Classify each message
MAP
Map Node
Archive the noise
EMAIL
Email Node
Send digest
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.