Mirra
BuildEvents

Event-Driven Scripts

Event-driven scripts automatically execute when specific events occur in your Mirra account. Scripts subscribe to events from integrated services like Telegram, Gmail, Google Calendar, voice calls, and crypto transactions, with support for complex filtering conditions to control when execution occurs.


Syntax

Creating an Event Subscription

POST /api/sdk/v1/scripts/{scriptId}/subscriptions

Basic Subscription

{
  "eventType": "telegram.message"
}

Filtered Subscription

{
  "eventType": "telegram.message",
  "conditions": [
    {
      "field": "content.text",
      "operator": "contains",
      "value": "urgent"
    }
  ]
}

Multiple Conditions

{
  "eventType": "telegram.message",
  "conditions": [
    {
      "field": "content.text",
      "operator": "contains",
      "value": "urgent"
    },
    {
      "field": "telegram.isGroupChat",
      "operator": "equals",
      "value": true
    }
  ]
}

Description

Event-driven scripts enable reactive automation by executing scripts in response to real-time events from integrated services. When an event occurs, the Mirra platform checks all active subscriptions, evaluates filter conditions, and executes matching scripts with the event data.

All events follow a standardized structure defined in @mirra/shared-types/events, ensuring consistent access patterns across different event types. The structure uses clear nesting with integration-specific data in typed objects, eliminating vague field names and minimizing optional fields.

Event Flow

1. Event Occurs (e.g., Telegram message received)

2. Event Published to Kafka Event System

3. Script Subscriptions Evaluated

4. Filter Conditions Checked

5. Matching Scripts Execute with Event Data

Event Subscriptions

Scripts create subscriptions to specific event types with optional filtering conditions. Each subscription specifies:

  • Event type - The specific event to subscribe to (e.g., telegram.message)
  • Conditions - Optional filters using field paths, operators, and values
  • Enabled status - Whether the subscription is currently active

Subscriptions are created via the SDK API:

curl -X POST https://api.getmirra.app/api/sdk/v1/scripts/SCRIPT_ID/subscriptions \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "telegram.message",
    "conditions": [
      {
        "field": "content.text",
        "operator": "contains",
        "value": "$AAPL"
      }
    ]
  }'

Event Data Structure

All events extend BaseIntegrationEvent and include integration-specific fields:

import type { IntegrationEvent } from '@mirra/shared-types/events';
 
interface BaseIntegrationEvent {
  // Core Identity (always present)
  id: string;
  type: EventType;               // e.g., 'telegram.message', 'gmail.email_received'
  source: EventSource;           // e.g., 'telegram', 'google-gmail'
  timestamp: Date;               // When the event occurred
  userId: string;                // Owner of the event (never optional)
  
  // Normalized Content (always present)
  content: {
    text: string;                // Main content - empty string if none
    contentType: ContentType;    // 'message' | 'email' | 'calendar_event' | 'call'
    timestamp: Date;             // When content was created
  };
  
  // Actor (who created this)
  actor: {
    id: string;                  // Actor ID - empty string if system-generated
    name: string;                // Display name - "System" if no actor
    actorType: ActorType;        // 'user' | 'bot' | 'system' | 'calendar' | 'email'
  };
  
  // Context (where this happened)
  context: {
    channelId: string;           // Chat ID, thread ID, call ID
    channelName: string;         // Chat name, subject, etc.
    channelType: ChannelType;    // 'direct_message' | 'group_chat' | 'email_thread'
  } | null;                      // Null for system events
  
  // Graph context
  graphId: string | null;        // Multi-tenant isolation
}

Standardized Fields

Every event includes these standardized fields for consistent access:

id (string)

Unique event identifier.

type (EventType)

Event type identifier (e.g., 'telegram.message', 'gmail.email_received').

source (EventSource)

Integration source (e.g., 'telegram', 'google-gmail').

timestamp (Date)

When the event occurred in the Mirra system.

userId (string)

User ID who owns this event. Always present.

content.text (string)

Main text content. Works across all event types. Empty string if no text.

content.contentType (ContentType)

Type of content: 'message', 'email', 'calendar_event', 'call', 'document', or 'transcript'.

content.timestamp (Date)

When the content was created in the source system.

actor.id (string)

ID of who created this event. Empty string for system-generated events.

actor.name (string)

Display name of the actor. "System" for system-generated events.

actor.actorType (ActorType)

Type of actor: 'user', 'bot', 'system', 'calendar', or 'email'.

context (ChannelContext | null)

Where the event occurred. Null for system events without channel context.

FieldTypeDescription
context.channelIdstringChat ID, thread ID, call ID, etc.
context.channelNamestringHuman-readable channel name
context.channelTypeChannelType'direct_message', 'group_chat', 'channel', 'email_thread', 'call'
entity (EntityData)

Graph entity data for memory and knowledge graph integration. Always present.

Integration-Specific Fields

Each event type includes additional fields specific to its integration:

  • Telegram events - telegram object with chat ID, message ID, media info
  • Gmail events - gmail object with message ID, thread ID, subject, recipients
  • Calendar events - calendar object with event ID, times, attendees
  • Voice call events - call object with call ID, participants, duration
  • Crypto events - crypto object with token address, chain, price

See the integration-specific documentation for complete field listings.

Accessing Event Data

Scripts receive events with both standardized and integration-specific fields:

export async function handler(event, context) {
  // Standardized fields (work for ANY event type)
  const text = event.content.text;
  const sender = event.actor.name;
  const timestamp = event.timestamp;
  
  // Integration-specific fields (type-safe access)
  if (event.type === 'telegram.message') {
    const chatId = event.telegram.chatId;
    const isGroup = event.telegram.isGroupChat;
  }
  
  if (event.type === 'gmail.email_received') {
    const subject = event.gmail.subject;
    const from = event.gmail.from.email;
    const hasAttachments = event.gmail.hasAttachments;
  }
  
  // Legacy fields (backward compatibility)
  const messageLegacy = event.messageText;  // Same as content.text
  const senderLegacy = event.senderName;    // Same as actor.name
  
  return { success: true };
}

Event Enrichment

Many events support automatic content enrichment through stream processing:

enrichment (EventEnrichment | null)

Enrichment data added by stream processing. Null until processing completes.

FieldTypeDescription
enrichment.tickersTickerMention[]Detected stock/crypto ticker symbols
enrichment.sentimentSentimentAnalysis | nullSentiment score (-1 to 1), label, confidence
enrichment.companiesstring[]Detected company names
enrichment.topicsstring[]Extracted topics
enrichment.privacyobjectPrivacy controls (isPublic, anonymizationLevel, allowedAggregations)
enrichment.streamobjectStream metadata (timestamps, partition, processingVersion)

Events that support enrichment:

  • telegram.message
  • telegram.command
  • gmail.email_received
  • google-docs.document_updated
  • transcript.chunk

Example:

export async function handler(event, context) {
  if (event.enrichment?.tickers) {
    for (const ticker of event.enrichment.tickers) {
      console.log(`Found ${ticker.symbol} (confidence: ${ticker.confidence})`);
    }
  }
  
  if (event.enrichment?.sentiment) {
    console.log(`Sentiment: ${event.enrichment.sentiment.label} (${event.enrichment.sentiment.score})`);
  }
  
  return { success: true };
}

Filtering Events

Event subscriptions support complex filtering with field-level conditions. Multiple conditions use AND logic by default.

Condition Operators

String Operators

OperatorDescriptionExample
equalsExact match{"field": "type", "operator": "equals", "value": "telegram.message"}
not_equalsNot equal{"field": "actor.name", "operator": "not_equals", "value": "Bot"}
containsContains substring{"field": "content.text", "operator": "contains", "value": "$"}
not_containsDoesn't contain{"field": "content.text", "operator": "not_contains", "value": "spam"}
starts_withStarts with prefix{"field": "content.text", "operator": "starts_with", "value": "/"}
ends_withEnds with suffix{"field": "content.text", "operator": "ends_with", "value": "?"}

Numeric Operators

OperatorDescriptionExample
greater_thanGreater than{"field": "crypto.priceUsd", "operator": "greater_than", "value": 100}
less_thanLess than{"field": "call.durationSeconds", "operator": "less_than", "value": 300}
greater_than_or_equalGreater or equal{"field": "enrichment.tickers.length", "operator": "greater_than_or_equal", "value": 1}
less_than_or_equalLess or equal{"field": "call.durationSeconds", "operator": "less_than_or_equal", "value": 600}

Existence Operators

OperatorDescriptionExample
existsField exists{"field": "enrichment.tickers", "operator": "exists"}
not_existsField doesn't exist{"field": "error", "operator": "not_exists"}

Array Operators

OperatorDescriptionExample
inValue in array{"field": "type", "operator": "in", "value": ["call.started", "call.ended"]}
not_inValue not in array{"field": "status", "operator": "not_in", "value": ["error", "timeout"]}

Logical Operators

OperatorDescriptionExample
andAll conditions must match{"operator": "and", "conditions": [...]}
orAny condition must match{"operator": "or", "conditions": [...]}

Complex Filtering Examples

Multiple conditions (AND):

{
  "eventType": "telegram.message",
  "conditions": [
    {
      "field": "content.text",
      "operator": "contains",
      "value": "urgent"
    },
    {
      "field": "telegram.isGroupChat",
      "operator": "equals",
      "value": true
    }
  ]
}

OR conditions:

{
  "conditions": [
    {
      "operator": "or",
      "conditions": [
        {
          "field": "type",
          "operator": "equals",
          "value": "call.started"
        },
        {
          "field": "type",
          "operator": "equals",
          "value": "call.ended"
        }
      ]
    }
  ]
}

Nested conditions:

{
  "eventType": "telegram.message",
  "conditions": [
    {
      "operator": "or",
      "conditions": [
        {
          "field": "content.text",
          "operator": "contains",
          "value": "urgent"
        },
        {
          "field": "content.text",
          "operator": "contains",
          "value": "important"
        }
      ]
    },
    {
      "field": "telegram.isGroupChat",
      "operator": "equals",
      "value": true
    }
  ]
}

Managing Subscriptions

List Subscriptions

Retrieve all subscriptions for a script:

curl https://api.getmirra.app/api/sdk/v1/scripts/SCRIPT_ID/subscriptions \
  -H "X-API-Key: YOUR_API_KEY"

Update Subscription

Modify an existing subscription:

curl -X PATCH https://api.getmirra.app/api/sdk/v1/scripts/SCRIPT_ID/subscriptions/SUB_ID \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false
  }'

Delete Subscription

Remove a subscription:

curl -X DELETE https://api.getmirra.app/api/sdk/v1/scripts/SCRIPT_ID/subscriptions/SUB_ID \
  -H "X-API-Key: YOUR_API_KEY"

Available Event Types

Communication Events

Productivity Events

System Events

Financial Events

  • Crypto - Price alerts, transactions, swaps

Execution Limits

Daily Limits

Scripts enforce daily execution limits configured in the script settings:

{
  "config": {
    "maxExecutionsPerDay": 100
  }
}

Once the limit is reached, the script stops executing until the next day (UTC midnight).

Cost Limits

Scripts also enforce per-execution cost limits:

{
  "config": {
    "maxCostPerExecution": 1.0
  }
}

Executions exceeding this cost fail with a cost limit error.


Best Practices

Be Specific with Filters

Use specific conditions to avoid unnecessary executions:

// ✅ Good - Specific filter
{
  "eventType": "telegram.message",
  "conditions": [
    {"field": "content.text", "operator": "contains", "value": "$AAPL"}
  ]
}
 
// ❌ Bad - Too broad
{
  "eventType": "telegram.message"
}

Handle Missing Fields

Always check for field existence before accessing:

export async function handler(event, context) {
  // Check if field exists
  if (!event.enrichment?.tickers) {
    return { success: true, message: 'No tickers found' };
  }
  
  // Process tickers
  for (const ticker of event.enrichment.tickers) {
    await processTicker(ticker);
  }
  
  return { success: true };
}

Use Appropriate Execution Limits

Set limits based on expected event volume:

{
  "config": {
    "maxExecutionsPerDay": 1000  // Adjust based on expected volume
  }
}

Test Before Enabling

Create subscriptions as disabled, test manually, then enable:

{
  "eventType": "telegram.message",
  "enabled": false  // Test first
}

See Also

On this page