Documentation

Complete guide for AT Protocol power users to understand and use If This Then AT://

⚠️ Alpha Software Notice

This service is alpha-quality software and is in active development.

By using this service, you acknowledge these limitations and risks.

Authentication

This website uses ATProtocol OAuth to support signing in. Specifically, we use the AIP (ATProtocol Identity Provider) authentication gateway to service ATProtocol OAuth and app-password sessions.

OAuth Scopes

This is an automation service that has broad features and uses the following OAuth scopes:

⚠️ Important: Session Limitations

ATProtocol OAuth sessions have limits as to how long they last. We highly recommend creating an app-password and setting it through the dashboard to allow your blueprints to run indefinitely.

Note: It is very important to understand that app-passwords do not have granular permissions.

Data Storage

📍 Where Your Data Lives

Important: All blueprints, nodes, and prototypes are stored in the If This Then AT:// application database, not in your PDS (Personal Data Server).

This means:

  • Your automation configurations are tied to this specific instance
  • Blueprints are not portable between different If This Then AT:// instances
  • Deleting your account here removes all your blueprints (but not your PDS data)
  • Your PDS only stores the records created by publish_record actions
Current Architecture

The service operates with a clear separation:

  • Application Database: Stores all blueprint definitions, node configurations, prototypes, and execution history
  • Your PDS: Only receives records when publish_record nodes execute
  • Authentication: Uses AT Protocol OAuth to act on your behalf

🔮 Future Considerations

We are currently evaluating approaches for a hybrid public-private data model that would allow:

  • Blueprints to be created and stored outside of a specific instance
  • Importing blueprints via XRPC calls
  • Portable automation definitions between instances
  • Public blueprint sharing while maintaining private configurations

Note: These features are under consideration and not yet implemented.

Blueprints

A blueprint is an automation definition that connects AT Protocol events to actions.

Blueprint Structure
  • Blueprints have one "entry" node that could be:
    • A Jetstream event watch
    • Webhook invocation
    • Zapier zap
    • Periodic trigger (via cron schedule)
  • Blueprints have one or more action nodes that include:
    • Making a webhook HTTP POST request
    • Creating a record in your PDS

A node is an individual step in a blueprint. There are several node types including jetstream_entry, webhook_entry, transform, condition, facet_text, parse_aturi, get_record, publish_record, and more.

Node Components: Nodes have two pieces of information:

  • Configuration: Node-specific settings (e.g., cron schedule, webhook URL)
  • Payload: Data transformation or filtering logic

Nodes use JSONLogic via the datalogic-rs crate to evaluate inputs and produce output. Not all nodes create output (action nodes like publish_webhook and publish_record are terminal).

Node Type: jetstream_entry

Filters Jetstream firehose events. Must be the first node in the blueprint. Processes real-time events from the AT Protocol network.

Configuration Options

Both string and array formats are supported for filters:

{
  // Single DID filter (string format)
  "did": "did:plc:xyz123",

  // Multiple DIDs filter (array format)
  "did": ["did:plc:xyz123", "did:web:example.com"],

  // Single collection filter (string format)
  "collection": "app.bsky.feed.post",

  // Multiple collections filter (array format)
  "collection": ["app.bsky.feed.post", "app.bsky.feed.like"]
}

Filter Behavior: When both did and collection filters are present, they use AND logic (both must match). If neither filter is configured, all events pass through to payload evaluation.

Supported DID Formats
  • did:plc: - Must be followed by exactly 24 lowercase alphanumeric characters
  • did:web: - Must include valid hostname and optional path segments
  • did:webvh: - WebVH DID format (rare)

Payload Options

The payload can be either a boolean or a JSONLogic expression:

// Boolean: Accept all events that pass configuration filters
true

// Boolean: Reject all events (useful for temporarily disabling)
false

// JSONLogic: Must evaluate to a boolean value
{
  "contains": [
    {"val": ["commit", "record", "text"]},
    "hello"
  ]
}
Input Event Structure

Jetstream events have this structure:

{
  "kind": "commit",
  "did": "did:plc:abc123...",
  "commit": {
    "collection": "app.bsky.feed.post",
    "operation": "create",
    "rkey": "3abc...",
    "cid": "bafyr...",
    "record": {
      "$type": "app.bsky.feed.post",
      "text": "Hello world!",
      "createdAt": "2024-01-01T00:00:00.000Z",
      "facets": []
    }
  },
  "time_us": 1234567890
}

Example 1: Filter posts from specific users

Configuration:

{
  "did": ["did:plc:alice123", "did:plc:bob456"],
  "collection": "app.bsky.feed.post"
}

Payload:

true

This accepts all posts from Alice or Bob only.

Example 2: Monitor posts with specific hashtags

Configuration:

{
  "collection": "app.bsky.feed.post"
}

Payload:

{
  "contains": [
    {"lower": [{"val": ["commit", "record", "text"]}]},
    "#atproto"
  ]
}

This filters for posts containing the hashtag #atproto (case-insensitive).

Example 3: Complex filter for posts with mentions

Configuration:

{
  "collection": ["app.bsky.feed.post"]
}

Payload:

{
  "and": [
    {"==": [{"val": ["commit", "operation"]}, "create"]},
    {
      ">": [
        {"len": {"val": ["commit", "record", "facets"]}},
        0
      ]
    }
  ]
}

This filters for newly created posts that have facets (mentions, links, etc.).

Example 4: Monitor posts with links to your website

Configuration:

{
  "collection": ["app.bsky.feed.post"]
}

Payload:

{
  "and": [
    {"==": [{"val": ["kind"]}, "commit"]},
    {"==": [{"val": ["commit", "operation"]}, "create"]},
    {
      "some": [
        {"val": ["commit", "record", "facets"]},
        {
          "some": [
            {"val": ["features"]},
            {
              "and": [
                {"==": [{"val": ["$type"]}, "app.bsky.richtext.facet#link"]},
                {"starts_with": [{"val": ["uri"]}, "https://example.com/"]}
              ]
            }
          ]
        }
      ]
    }
  ]
}

This filters for new posts containing links to your website.

Node Type: webhook_entry

Receives HTTP POST webhooks and processes them through your blueprint. Must be the first node in the blueprint.

Configuration

The configuration must be an empty object - no configuration options are currently supported:

{}

Payload Options

The payload can be either a boolean or a JSONLogic expression that evaluates to boolean:

// Boolean: Accept all webhook requests
true

// Boolean: Reject all webhook requests (useful for temporarily disabling)
false

// JSONLogic: Must evaluate to a boolean value
{
  "==": [
    {"val": ["headers", "content-type"]},
    "application/json"
  ]
}
Webhook Request Structure

Webhook requests are passed to the node with this structure:

{
  "method": "POST",
  "path": "/webhooks/blueprint-id",
  "headers": {
    "content-type": "application/json",
    "x-webhook-signature": "..."
  },
  "body": {
    // The parsed JSON body of the webhook request
  },
  "query": {
    // Query parameters from the URL
  }
}

Example 1: Accept all webhook requests

Configuration:

{}

Payload:

true

This accepts all incoming webhook requests.

Example 2: Filter by content type and method

Configuration:

{}

Payload:

{
  "and": [
    {"==": [{"val": ["method"]}, "POST"]},
    {"==": [{"val": ["headers", "content-type"]}, "application/json"]}
  ]
}

This only accepts POST requests with JSON content type.

Example 3: Validate webhook signature presence

Configuration:

{}

Payload:

{
  "and": [
    {"!=": [{"val": ["headers", "x-webhook-signature"]}, null]},
    {"!=": [{"val": ["body", "signature"]}, null]}
  ]
}

This ensures both header signature and body signature are present.

Example 4: Filter by event type in body

Configuration:

{}

Payload:

{
  "==": [
    {"val": ["body", "event_type"]},
    "user.created"
  ]
}

This only processes webhooks with specific event type.

Example 5: Complex multi-condition filter

Configuration:

{}

Payload:

{
  "and": [
    {"==": [{"val": ["method"]}, "POST"]},
    {"==": [{"val": ["headers", "content-type"]}, "application/json"]},
    {"in": [{"val": ["body", "action"]}, ["create", "update"]]},
    {">": [{"val": ["body", "priority"]}, 5]}
  ]
}

This validates method, content type, allowed actions, and priority threshold.

Example 6: Validate with query parameters

Configuration:

{}

Payload:

{
  "and": [
    {"==": [{"val": ["query", "token"]}, "secret123"]},
    {"==": [{"val": ["query", "version"]}, "v2"]}
  ]
}

This validates authentication token and API version from query parameters.

⚠️ Important Webhook Details

  • Webhook URL Format: https://ifthisthen.at/webhooks/{blueprint_record_key}
  • Replace {blueprint_record_key} with your blueprint's record key (the last segment of the AT-URI)
  • Only POST requests are accepted
  • Request body must be valid JSON
  • The webhook handler validates that the first node is a webhook_entry node
  • Successful webhook requests return HTTP 202 (Accepted) status
  • Throttled requests return HTTP 200 (OK) with a "throttled" status
  • Invalid blueprints or requests return appropriate error codes (400, 404, etc.)
Response Formats

Success Response (HTTP 202):

{
  "status": "accepted",
  "message": "Blueprint evaluation queued",
  "blueprint": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123"
}

Throttled Response (HTTP 200):

{
  "status": "throttled",
  "message": "Request accepted but throttled",
  "blueprint": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123"
}

Error Response (HTTP 4xx/5xx):

{
  "error": "Blueprint not found",
  "message": "No blueprint found with record key: abc123"
}

Node Type: periodic_entry

Triggers blueprints on a schedule using cron expressions. Must be the first node in the blueprint.

⚠️ Important Scheduling Limits

  • Minimum interval: 30 minutes (1800 seconds)
  • Maximum interval: 90 days (7,776,000 seconds)
  • Schedules outside these bounds will be rejected

Configuration

The configuration requires a cron expression:

{
  "cron": "0 * * * *"  // Required: Standard cron expression
}
Cron Expression Format

Standard 5-field format: MIN HOUR DAY MONTH WEEKDAY

  • MIN: Minutes (0-59)
  • HOUR: Hours (0-23)
  • DAY: Day of month (1-31)
  • MONTH: Month (1-12)
  • WEEKDAY: Day of week (0-7, where 0 and 7 are Sunday)

Special strings supported:

  • @hourly - Run once an hour (0 * * * *)
  • @daily - Run once a day (0 0 * * *)
  • @weekly - Run once a week (0 0 * * 0)
  • @monthly - Run once a month (0 0 1 * *)
  • @yearly - ❌ Not allowed (exceeds 90-day limit)

Payload Options

The payload determines whether the scheduled blueprint should run:

// Boolean: Always run when scheduled
true

// Boolean: Never run (useful for temporarily disabling)
false

// JSONLogic: Conditional execution based on time or other factors
{
  ">": [{"format_date": [{"now": []}, "HH"]}, "08"]  // Only run after 8 AM
}

Example 1: Every 30 minutes (minimum allowed)

Configuration:

{
  "cron": "0,30 * * * *"
}

Payload:

true

Example 2: Daily status post at 9 AM

Configuration:

{
  "cron": "0 9 * * *"
}

Payload (generates event data):

{
  "event_type": "daily_status",
  "timestamp": {"now": []},
  "message": "Daily automated status update"
}

Example 3: Every 2 hours

Configuration:

{
  "cron": "0 */2 * * *"
}

Payload:

true

Example 4: Weekly on Monday at noon

Configuration:

{
  "cron": "0 12 * * 1"
}

Payload:

{
  "type": "weekly_summary",
  "week": {"format_date": [{"now": []}, "W"]}
}

Example 5: First day of each month

Configuration:

{
  "cron": "0 0 1 * *"
}

Payload:

{
  "type": "monthly_report",
  "month": {"format_date": [{"now": []}, "YYYY-MM"]}
}

❌ Invalid Examples

  • "* * * * *" - Every minute (too frequent, minimum is 30 minutes)
  • "*/5 * * * *" - Every 5 minutes (too frequent)
  • "0 0 1 1 *" - Yearly on Jan 1 (exceeds 90-day maximum)
  • "@yearly" - Yearly special string (exceeds 90-day maximum)

Node Type: condition

Provides boolean flow control - acts as a gate that either allows data to pass through or stops processing.

Configuration

The configuration must be an empty object:

{}

Payload Options

The payload determines whether the blueprint should proceed:

// Boolean: Always allow data through
true

// Boolean: Always stop processing (useful for temporarily disabling)
false

// JSONLogic: Must evaluate to a boolean value
{
  "==": [{"val": ["status"]}, "active"]
}
How Condition Works
  • If payload evaluates to true: Data continues through pipeline unchanged
  • If payload evaluates to false: Pipeline stops (returns None)
  • Non-boolean results from JSONLogic cause an error

Example 1: Simple equality check

Payload:

{
  "==": [{"val": ["status"]}, "active"]
}

Only continues if status field equals "active".

Example 2: Range validation

Payload:

{
  "and": [
    {">": [{"val": ["count"]}, 0]},
    {"<=": [{"val": ["count"]}, 100]}
  ]
}

Checks if count is between 1 and 100.

Example 3: Only process posts with images

Payload:

{
  "or": [
    {"==": [{"val": ["commit", "record", "embed", "$type"]}, "app.bsky.embed.images"]},
    {"==": [{"val": ["commit", "record", "embed", "$type"]}, "app.bsky.embed.external"]}
  ]
}

Continues only if post has image or external embeds.

Example 4: Text contains check

Payload:

{
  "contains": [{"val": ["text"]}, "keyword"]
}

Checks if text field contains specific keyword.

Example 5: Complex nested conditions

Payload:

{
  "or": [
    {"==": [{"val": ["priority"]}, "high"]},
    {"and": [
      {"==": [{"val": ["priority"]}, "medium"]},
      {">": [{"val": ["score"]}, 75]}
    ]}
  ]
}

Accepts high priority items OR medium priority with score > 75.

Example 6: Check for field existence

Payload:

{
  "!=": [{"val": ["metadata", "tags"]}, null]
}

Continues only if metadata.tags field exists (is not null).

Node Type: transform

Transforms data passing through the blueprint pipeline. Transform nodes reshape, extract, and modify data as it flows through the pipeline.

⚠️ Important Requirements

  • The payload MUST be either:
    • A JSON object containing DataLogic expressions (single transformation)
    • An array of JSON objects for chained transformations (output of each becomes input to next)
  • The configuration MUST be an empty object {}
  • The evaluation result MUST be an object that replaces the input

Configuration

The configuration must be an empty object:

{}
How Transform Works

Transform nodes apply DataLogic templates to create new data structures from input data. The output completely replaces the input for subsequent nodes.

Single transformation (object payload): Input → Transform → Output

Chained transformations (array payload): Input → Transform₁ → Transform₂ → ... → Output

When using an array of transformations, each transformation's output becomes the input for the next transformation in the chain.

Example 1: Extract specific fields

Payload:

{
  "id": {"val": ["user", "id"]},
  "name": {"val": ["user", "profile", "displayName"]},
  "timestamp": {"val": ["createdAt"]}
}

Extracts and restructures specific fields from nested input data.

Example 2: Create a repost record for publish_record

Payload:

{
  "record": {
    "$type": "app.bsky.feed.repost",
    "subject": {
      "cid": {"val": ["commit", "cid"]},
      "uri": {
        "cat": [
          "at://",
          {"val": ["did"]},
          "/app.bsky.feed.post/",
          {"val": ["commit", "rkey"]}
        ]
      }
    },
    "createdAt": {"now": []}
  }
}

Note: The record must be wrapped in a "record" field for publish_record nodes.

Example 3: Conditional transformation

Payload:

{
  "status": {
    "if": [
      {">": [{"val": ["score"]}, 100]},
      "excellent",
      {"if": [
        {">": [{"val": ["score"]}, 50]},
        "good",
        "needs improvement"
      ]}
    ]
  },
  "score": {"val": ["score"]}
}

Creates conditional fields based on input values.

Example 4: String concatenation

Payload:

{
  "message": {
    "cat": [
      "User ",
      {"val": ["username"]},
      " posted: ",
      {"val": ["text"]}
    ]
  }
}

Concatenates strings and values to create formatted messages.

Example 5: Array operations

Payload:

{
  "items": {
    "map": [
      {"val": ["products"]},
      {
        "name": {"val": ["title"]},
        "price_with_tax": {"*": [{"val": ["price"]}, 1.1]}
      }
    ]
  },
  "total": {"sum": {"val": ["products", "*", "price"]}}
}

Maps over arrays and calculates aggregates.

Example 6: Chained transformations (array payload)

Payload: An array of transformation objects, each applied sequentially:

[
  {
    "greeting": {"cat": ["Hello, ", {"val": ["name"]}, " from ", {"val": ["city"]}]},
    "is_newyorker": {"==": [{"val": ["city"]}, "New York"]},
    "voting_age": {">=": [{"val": ["age"]}, 18]}
  },
  {
    "greeting": {"val": ["greeting"]},
    "can_vote": {"and": [{"==": [{"val": ["voting_age"]}, true]}, {"==": [{"val": ["is_newyorker"]}, true]}]}
  }
]

Input:

{"name": "Alice", "age": 25, "city": "New York"}

First transformation output (becomes input to second):

{
  "greeting": "Hello, Alice from New York",
  "is_newyorker": true,
  "voting_age": true
}

Final output:

{
  "greeting": "Hello, Alice from New York",
  "can_vote": true
}

The first transformation creates intermediate fields, and the second transformation uses those to create the final output. This allows complex multi-step data processing in a single transform node.

Node Type: facet_text

Parses text to extract mentions, URLs, and hashtags, creating AT Protocol facets for rich text features. This node resolves @mentions to DIDs and generates proper facet structures required by AT Protocol applications.

⚠️ Important Features

  • Extracts @mentions, URLs, and #hashtags from text
  • Resolves handles to DIDs using the identity resolver
  • Creates byte-range facets for proper text highlighting
  • Unresolvable handles are skipped (rendered as plain text)
  • Purely numeric hashtags (e.g., #123) are excluded
  • Supports both regular (#) and fullwidth (#) hash symbols
  • Clones input and adds result to specified destination field

Configuration

Optional configuration to specify where to store the result:

// Default: stores result in "text" field
{}

// Custom destination field
{
  "destination": "processed"
}

Payload Options

The payload determines how to extract the text to process:

// String: Field name to extract text from
"content"

// Object: JSONLogic expression to extract text
{"val": ["record", "text"]}
Output Structure

The node clones the input and adds a facet result object to the destination field:

{
  "...original_input_fields...",
  "destination_field": {
    "text": "The original text content",
    "facets": [
      {
        "index": {"byteStart": 6, "byteEnd": 24},
        "features": [
          {
            "$type": "app.bsky.richtext.facet#mention",
            "did": "did:plc:resolved_did"
          }
        ]
      },
      {
        "index": {"byteStart": 36, "byteEnd": 55},
        "features": [
          {
            "$type": "app.bsky.richtext.facet#link",
            "uri": "https://example.com"
          }
        ]
      }
    ]
  }
}
Mention Pattern

Mentions must follow AT Protocol handle syntax:

  • Start with @ symbol
  • Format: @handle.domain.tld
  • Example: @alice.bsky.social
  • Must have at least one dot in the handle
  • Can appear at the start of text or after non-word characters
URL Pattern

URLs are detected with these requirements:

  • Must start with http:// or https://
  • Must contain a valid domain with TLD
  • Can include paths, query parameters, and fragments
  • Can appear at the start of text or after non-word characters
Hashtag Pattern

Hashtags are detected with these requirements:

  • Start with # or # (fullwidth) symbol
  • Followed by word characters (letters, numbers, underscores)
  • Cannot be purely numeric (e.g., #123 is excluded)
  • Can appear at the start of text or after non-word characters
  • Examples: #rust, #atproto, #test123

Example 1: Process text with mentions, URLs, and hashtags

Configuration:

{}

Payload:

"text"

Input:

{
  "text": "Hello @alice.bsky.social! Check out https://example.com #rust #atproto",
  "other": "data"
}

Output:

{
  "other": "data",
  "text": {
    "text": "Hello @alice.bsky.social! Check out https://example.com #rust #atproto",
    "facets": [
      {
        "index": {"byteStart": 6, "byteEnd": 24},
        "features": [{
          "$type": "app.bsky.richtext.facet#mention",
          "did": "did:plc:alice123"
        }]
      },
      {
        "index": {"byteStart": 36, "byteEnd": 55},
        "features": [{
          "$type": "app.bsky.richtext.facet#link",
          "uri": "https://example.com"
        }]
      },
      {
        "index": {"byteStart": 56, "byteEnd": 61},
        "features": [{
          "$type": "app.bsky.richtext.facet#tag",
          "tag": "rust"
        }]
      },
      {
        "index": {"byteStart": 62, "byteEnd": 70},
        "features": [{
          "$type": "app.bsky.richtext.facet#tag",
          "tag": "atproto"
        }]
      }
    ]
  }
}

Note: Original input fields are preserved, and the facet result is added to the "text" field (default destination).

Example 2: Custom destination field

Configuration:

{
  "destination": "processed"
}

Payload:

"content"

Input:

{
  "content": "Visit https://test.org",
  "existing": "preserved"
}

Output:

{
  "content": "Visit https://test.org",
  "existing": "preserved",
  "processed": {
    "text": "Visit https://test.org",
    "facets": [
      {
        "index": {"byteStart": 6, "byteEnd": 22},
        "features": [{
          "$type": "app.bsky.richtext.facet#link",
          "uri": "https://test.org"
        }]
      }
    ]
  }
}

Example 3: Using JSONLogic to extract text

Configuration:

{
  "destination": "facets_result"
}

Payload:

{"val": ["record", "text"]}

Input:

{
  "record": {
    "text": "Hello @alice.bsky.social!"
  },
  "other": "data"
}

Output:

{
  "record": {
    "text": "Hello @alice.bsky.social!"
  },
  "other": "data",
  "facets_result": {
    "text": "Hello @alice.bsky.social!",
    "facets": [
      {
        "index": {"byteStart": 6, "byteEnd": 24},
        "features": [{
          "$type": "app.bsky.richtext.facet#mention",
          "did": "did:plc:alice123"
        }]
      }
    ]
  }
}

Example 4: Complete blueprint with facet_text

{
  "nodes": [
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "content": "Hello @alice.bsky.social! Check out https://example.com"
      }
    },
    {
      "type": "facet_text",
      "configuration": {"destination": "processed"},
      "payload": "content"
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "record": {
          "$type": "app.bsky.feed.post",
          "text": {"val": ["processed", "text"]},
          "facets": {"val": ["processed", "facets"]},
          "createdAt": {"now": []}
        }
      }
    },
    {
      "type": "publish_record",
      "configuration": {},
      "payload": "record"
    }
  ]
}

This blueprint creates a post with properly formatted mentions and links.

⚠️ Limitations

  • Hashtags are NOT processed (only mentions and URLs)
  • Handles that cannot be resolved to DIDs are skipped
  • Payload must be a string (field name) or object (JSONLogic)
  • The extracted text must be a string value
  • Byte positions are calculated for UTF-8 encoded text

Node Type: parse_aturi

Parses AT-URI strings to extract their component parts: repository (DID), collection, and record key. This node is essential for routing decisions, validating AT-URIs, and extracting structured data from AT Protocol identifiers.

⚠️ Important Features

  • Parses AT-URIs in the format: at://[repository]/[collection]/[record_key]
  • Extracts repository (typically a DID), collection, and record_key components
  • Validates AT-URI format and structure
  • Clones input and adds parsed result to specified destination field
  • Supports both string field extraction and JSONLogic evaluation

Configuration

Optional configuration to specify where to store the result:

// Default: stores result in "parsed_aturi" field
{}

// Custom destination field
{
  "destination": "parsed"
}

Payload Options

The payload determines how to extract the AT-URI to parse:

// String: Field name to extract AT-URI from
"uri"

// Object: JSONLogic expression to extract AT-URI
{"val": ["commit", "record", "subject", "uri"]}
Parsed Result Structure

The node returns an object with three fields:

  • repository: The repository identifier (typically a DID)
  • collection: The collection/lexicon identifier (e.g., app.bsky.feed.post)
  • record_key: The record key/identifier

Example 1: Parse a simple AT-URI

Configuration:

{}

Payload:

"uri"

Input:

{
  "uri": "at://did:plc:lehcqqkwzcwvjvw66uthu5oq/community.lexicon.calendar.event/3lte3c7x43l2e",
  "other": "data"
}

Output:

{
  "uri": "at://did:plc:lehcqqkwzcwvjvw66uthu5oq/community.lexicon.calendar.event/3lte3c7x43l2e",
  "other": "data",
  "parsed_aturi": {
    "repository": "did:plc:lehcqqkwzcwvjvw66uthu5oq",
    "collection": "community.lexicon.calendar.event",
    "record_key": "3lte3c7x43l2e"
  }
}

Note: Original input fields are preserved, and the parsed result is added to the "parsed_aturi" field (default destination).

Example 2: Extract AT-URI from nested data

Configuration:

{
  "destination": "parsed"
}

Payload:

{"val": ["commit", "record", "subject", "uri"]}

Input:

{
  "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "commit": {
    "record": {
      "subject": {
        "uri": "at://did:plc:lehcqqkwzcwvjvw66uthu5oq/community.lexicon.calendar.event/3lte3c7x43l2e"
      }
    }
  }
}

Output:

{
  "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "commit": {
    "record": {
      "subject": {
        "uri": "at://did:plc:lehcqqkwzcwvjvw66uthu5oq/community.lexicon.calendar.event/3lte3c7x43l2e"
      }
    }
  },
  "parsed": {
    "repository": "did:plc:lehcqqkwzcwvjvw66uthu5oq",
    "collection": "community.lexicon.calendar.event",
    "record_key": "3lte3c7x43l2e"
  }
}

Example 3: Using parsed data for routing

{
  "nodes": [
    {
      "type": "webhook_entry",
      "configuration": {},
      "payload": true
    },
    {
      "type": "parse_aturi",
      "configuration": {"destination": "parsed"},
      "payload": {"val": ["body", "subject_uri"]}
    },
    {
      "type": "condition",
      "configuration": {},
      "payload": {
        "==": [
          {"val": ["parsed", "collection"]},
          "community.lexicon.calendar.event"
        ]
      }
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "event_id": {"val": ["parsed", "record_key"]},
        "event_did": {"val": ["parsed", "repository"]},
        "is_calendar_event": true
      }
    }
  ]
}

This blueprint accepts webhooks, parses AT-URIs, and routes based on the collection type.

Example 4: Building AT-URIs from parsed components

{
  "nodes": [
    {
      "type": "parse_aturi",
      "configuration": {},
      "payload": "original_uri"
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "new_uri": {
          "cat": [
            "at://",
            {"val": ["parsed_aturi", "repository"]},
            "/app.bsky.feed.repost/",
            {"crockford": [{"now": []}]}
          ]
        },
        "original_collection": {"val": ["parsed_aturi", "collection"]}
      }
    }
  ]
}

This parses an AT-URI and builds a new one with a different collection.

Node Type: get_record

Fetches existing records from AT Protocol repositories by AT-URI. This node resolves the authority (DID or handle) to its PDS endpoint and retrieves the specified record content.

⚠️ Important Features

  • Fetches records from AT Protocol repositories using AT-URIs
  • Automatically resolves DIDs and handles to PDS endpoints
  • Validates AT-URI format and structure
  • Returns the fetched record with URI, CID, and value
  • Supports both string field extraction and JSONLogic evaluation

Configuration

The configuration is optional and may contain:

// Default destination field
{}

// Custom destination field
{
  "destination": "fetched_record"
}

The destination field specifies where the fetched record will be stored in the output (default: "get_record_result").

Payload Options

The payload determines how to extract the AT-URI:

// String: Field name containing the AT-URI
"uri"

// Object: JSONLogic expression to extract AT-URI
{"val": ["subject", "uri"]}

// Object: Extract from nested field path
{"val": ["commit", "record", "subject", "uri"]}
AT-URI Requirements
  • Must be a valid AT-URI format: at://[authority]/[collection]/[record_key]
  • Authority can be a DID or handle
  • Must include both collection and record_key
  • The authority must resolve to a valid PDS endpoint
Output Format

The node outputs the original input with the fetched record at the destination:

{
  "...original_input_fields...",
  "get_record_result": {
    "uri": "at://did:plc:abc/app.bsky.feed.post/xyz",
    "cid": "bafyrei...",
    "value": {
      "$type": "app.bsky.feed.post",
      "text": "Hello world!",
      "createdAt": "2024-01-01T00:00:00Z"
    }
  }
}

Example 1: Fetch a record from a liked post

Configuration:

{
  "destination": "original_post"
}

Payload:

{"val": ["commit", "record", "subject", "uri"]}

Expected Input (from jetstream_entry):

{
  "commit": {
    "record": {
      "$type": "app.bsky.feed.like",
      "subject": {
        "uri": "at://did:plc:test/app.bsky.feed.post/abc123",
        "cid": "bafyrei..."
      }
    }
  }
}

Output:

{
  "commit": { ... },
  "original_post": {
    "uri": "at://did:plc:test/app.bsky.feed.post/abc123",
    "cid": "bafyrei...",
    "value": {
      "$type": "app.bsky.feed.post",
      "text": "Original post content",
      "createdAt": "2024-01-01T00:00:00Z"
    }
  }
}

Example 2: Chain record fetching (complete blueprint)

{
  "nodes": [
    {
      "type": "jetstream_entry",
      "configuration": {"collection": ["app.bsky.feed.like"]},
      "payload": true
    },
    {
      "type": "get_record",
      "configuration": {"destination": "liked_post"},
      "payload": {"val": ["commit", "record", "subject", "uri"]}
    },
    {
      "type": "condition",
      "configuration": {},
      "payload": {
        "contains": [
          {"val": ["liked_post", "value", "text"]},
          "#automation"
        ]
      }
    },
    {
      "type": "publish_webhook",
      "configuration": {},
      "payload": {
        "url": "https://webhook.site/...",
        "method": "POST",
        "headers": {"Content-Type": "application/json"},
        "body": {
          "event": "liked_post_with_hashtag",
          "post": {"val": ["liked_post"]}
        }
      }
    }
  ]
}

This blueprint monitors likes, fetches the liked post content, and sends a webhook if the post contains #automation.

Example 3: Fetch record from webhook input

{
  "nodes": [
    {
      "type": "webhook_entry",
      "configuration": {},
      "payload": true
    },
    {
      "type": "get_record",
      "configuration": {},
      "payload": "record_uri"
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "response": {
          "fetched_text": {"val": ["get_record_result", "value", "text"]},
          "fetched_author": {"val": ["get_record_result", "uri"]}
        }
      }
    },
    {
      "type": "debug_action",
      "configuration": {},
      "payload": {}
    }
  ]
}

This blueprint accepts a webhook with a record_uri field, fetches the record, and transforms the response.

Node Type: sentiment_analysis

Analyzes text to detect emotional sentiment using deep learning. This node uses a BERT-based model to classify text into emotional categories with confidence scores.

⚠️ Important Features

  • Uses a pre-trained BERT model for emotion classification
  • Detects emotions like happiness, sadness, anger, love, surprise, fear, and neutral
  • Returns confidence scores for each emotion
  • Identifies the dominant emotion with its confidence level
  • Text is automatically truncated to 2000 characters for model compatibility
  • Model is loaded once and cached for performance

Configuration

Optional configuration to specify where to store the result:

// Default: stores result in "sentiment" field
{}

// Custom destination field
{
  "destination": "emotions"
}

Payload Options

The payload determines how to extract the text to analyze:

// String: Field name to extract text from
"text"

// Object: JSONLogic expression to extract text
{"val": ["post", "content"]}

// Extract from nested path
{"val": ["commit", "record", "text"]}
Output Structure

The node adds sentiment analysis results to the specified destination field:

{
  "...original_input_fields...",
  "sentiment": {
    "anger": 0.05,
    "disgust": 0.02,
    "fear": 0.08,
    "happiness": 0.65,
    "love": 0.12,
    "neutral": 0.10,
    "sadness": 0.05,
    "surprise": 0.05,
    "dominant_emotion": "happiness",
    "confidence": 0.65
  }
}
Available Emotions

The model can detect the following emotional categories:

  • happiness - Joy, satisfaction, contentment
  • sadness - Sorrow, grief, disappointment
  • anger - Rage, frustration, annoyance
  • love - Affection, caring, tenderness
  • fear - Anxiety, worry, apprehension
  • surprise - Astonishment, amazement, shock
  • disgust - Revulsion, distaste, aversion
  • neutral - No strong emotional content

Each emotion receives a confidence score between 0.0 and 1.0.

Example 1: Analyze webhook text

Configuration:

{}

Payload:

"message"

Input:

{
  "message": "I absolutely love this new feature! It makes me so happy!",
  "user": "alice"
}

Output:

{
  "message": "I absolutely love this new feature! It makes me so happy!",
  "user": "alice",
  "sentiment": {
    "happiness": 0.72,
    "love": 0.18,
    "neutral": 0.05,
    "sadness": 0.02,
    "anger": 0.01,
    "fear": 0.01,
    "surprise": 0.01,
    "dominant_emotion": "happiness",
    "confidence": 0.72
  }
}

Example 2: Analyze post sentiment from Jetstream

Configuration:

{
  "destination": "post_sentiment"
}

Payload:

{"val": ["commit", "record", "text"]}

Input (from jetstream_entry):

{
  "commit": {
    "record": {
      "$type": "app.bsky.feed.post",
      "text": "This is terrible news. I'm so disappointed and angry about this.",
      "createdAt": "2024-01-01T00:00:00Z"
    }
  },
  "did": "did:plc:test123"
}

Output:

{
  "commit": {...},
  "did": "did:plc:test123",
  "post_sentiment": {
    "anger": 0.45,
    "sadness": 0.35,
    "disgust": 0.10,
    "neutral": 0.05,
    "happiness": 0.02,
    "fear": 0.02,
    "surprise": 0.01,
    "dominant_emotion": "anger",
    "confidence": 0.45
  }
}

Example 3: Filter posts by positive sentiment

{
  "nodes": [
    {
      "type": "jetstream_entry",
      "configuration": {"collection": "app.bsky.feed.post"},
      "payload": true
    },
    {
      "type": "sentiment_analysis",
      "configuration": {"destination": "emotions"},
      "payload": {"val": ["commit", "record", "text"]}
    },
    {
      "type": "condition",
      "configuration": {},
      "payload": {
        "or": [
          {">=": [{"val": ["emotions", "happiness"]}, 0.5]},
          {">=": [{"val": ["emotions", "love"]}, 0.5]}
        ]
      }
    },
    {
      "type": "publish_webhook",
      "configuration": {"url": "https://webhook.site/positive-posts"},
      "payload": {
        "text": {"val": ["commit", "record", "text"]},
        "sentiment": {"val": ["emotions"]},
        "author": {"val": ["did"]}
      }
    }
  ]
}

This blueprint monitors posts, analyzes their sentiment, and only forwards posts with positive emotions (happiness or love > 50%).

Example 4: Route based on emotion type

{
  "nodes": [
    {
      "type": "webhook_entry",
      "configuration": {},
      "payload": true
    },
    {
      "type": "sentiment_analysis",
      "configuration": {},
      "payload": "text"
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "webhook_url": {
          "if": [
            {"in": [
              {"val": ["sentiment", "dominant_emotion"]},
              ["happiness", "love", "surprise"]
            ]},
            "https://api.example.com/positive",
            {"if": [
              {"in": [
                {"val": ["sentiment", "dominant_emotion"]},
                ["anger", "sadness", "fear"]
              ]},
              "https://api.example.com/negative",
              "https://api.example.com/neutral"
            ]}
          ]
        },
        "data": {"val": []}
      }
    },
    {
      "type": "publish_webhook",
      "configuration": {},
      "payload": {
        "url": {"val": ["webhook_url"]},
        "body": {"val": ["data"]}
      }
    }
  ]
}

This blueprint analyzes incoming webhook text and routes to different endpoints based on the dominant emotion.

Example 5: Emotion-based auto-response

{
  "nodes": [
    {
      "type": "jetstream_entry",
      "configuration": {
        "collection": "app.bsky.feed.post",
        "did": ["did:plc:specific-user"]
      },
      "payload": true
    },
    {
      "type": "sentiment_analysis",
      "configuration": {},
      "payload": {"val": ["commit", "record", "text"]}
    },
    {
      "type": "condition",
      "configuration": {},
      "payload": {
        "and": [
          {">=": [{"val": ["sentiment", "sadness"]}, 0.6]},
          {">=": [{"val": ["sentiment", "confidence"]}, 0.6]}
        ]
      }
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "record": {
          "$type": "app.bsky.feed.post",
          "text": "Sending you virtual hugs! 🤗 Hope things get better soon.",
          "reply": {
            "root": {
              "uri": {
                "cat": [
                  "at://",
                  {"val": ["did"]},
                  "/app.bsky.feed.post/",
                  {"val": ["commit", "rkey"]}
                ]
              },
              "cid": {"val": ["commit", "cid"]}
            },
            "parent": {
              "uri": {
                "cat": [
                  "at://",
                  {"val": ["did"]},
                  "/app.bsky.feed.post/",
                  {"val": ["commit", "rkey"]}
                ]
              },
              "cid": {"val": ["commit", "cid"]}
            }
          },
          "createdAt": {"now": []}
        }
      }
    },
    {
      "type": "publish_record",
      "configuration": {},
      "payload": "record"
    }
  ]
}

This blueprint monitors a specific user's posts, detects when they're feeling sad (>60% confidence), and automatically replies with a supportive message.

Node Type: publish_record

Creates records in your AT Protocol repository (PDS). This is an action node that publishes new records or updates existing ones.

⚠️ Important Changes

  • The record's $type field determines the collection (not configuration)
  • Configuration only supports optional record_key field
  • Payload can be a string (field name) or object (JSONLogic expression)
  • Output includes the original input with added publish_record_results

Configuration

The configuration is optional and may contain:

// Default: Auto-generated record key
{}

// Or with specific record key (for updates)
{
  "record_key": "custom-key-123"
}

If record_key is provided, it will update an existing record. Otherwise, a new record is created with an auto-generated key.

Payload Options

The payload determines how to extract the record data:

// String: Field name to extract from input
"record"

// Object: JSONLogic expression to extract record
{"val": ["record"]}

// Object: Pass through entire input
{"val": []}

// Object: Build record with JSONLogic
{
  "$type": "app.bsky.feed.post",
  "text": {"val": ["message"]},
  "createdAt": {"now": []}
}
Record Requirements
  • Record must be an object (not string, array, etc.)
  • Must contain $type field specifying the record type
  • The $type determines the collection (e.g., "app.bsky.feed.post")
  • Must include all required fields for the specific record type
Common Record Types
  • app.bsky.feed.post - Text posts with optional facets
  • app.bsky.feed.like - Likes on posts
  • app.bsky.feed.repost - Reposts of posts
  • app.bsky.graph.follow - Follow relationships
  • app.bsky.graph.block - Block relationships
  • Custom app-specific record types
Output Format

The node outputs the original input with added results:

{
  "...original_input_fields...",
  "publish_record_results": {
    "uri": "at://did:plc:abc/app.bsky.feed.post/xyz",
    "cid": "bafyrei..."
  }
}

Example 1: Publish a simple post

Configuration:

{}

Payload:

"record"

Expected Input:

{
  "record": {
    "$type": "app.bsky.feed.post",
    "text": "Hello from automation!",
    "createdAt": "2024-01-01T00:00:00Z",
    "langs": ["en"]
  }
}

Example 2: Auto-like posts (complete blueprint)

{
  "nodes": [
    {
      "type": "jetstream_entry",
      "configuration": {"collection": ["app.bsky.feed.post"]},
      "payload": {
        "contains": [{"val": ["commit", "record", "text"]}, "#ifthisthenat"]
      }
    },
    {
      "type": "transform",
      "configuration": {},
      "payload": {
        "record": {
          "$type": "app.bsky.feed.like",
          "subject": {
            "uri": {
              "cat": [
                "at://",
                {"val": ["did"]},
                "/app.bsky.feed.post/",
                {"val": ["commit", "rkey"]}
              ]
            },
            "cid": {"val": ["commit", "cid"]}
          },
          "createdAt": {"now": []}
        }
      }
    },
    {
      "type": "publish_record",
      "configuration": {},
      "payload": "record"
    }
  ]
}

This blueprint watches for posts containing "#ifthisthenat" and automatically likes them.

Example 3: Create a repost

Transform node output (publish_record input):

{
  "record": {
    "$type": "app.bsky.feed.repost",
    "subject": {
      "uri": "at://did:plc:xyz/app.bsky.feed.post/abc123",
      "cid": "bafyrei..."
    },
    "createdAt": "2024-01-01T12:00:00.000Z"
  }
}

Publish record configuration:

{}

Publish record payload:

"record"

Example 4: Follow a user

Record structure:

{
  "record": {
    "$type": "app.bsky.graph.follow",
    "subject": "did:plc:user-to-follow",
    "createdAt": "2024-01-01T12:00:00.000Z"
  }
}

Example 5: Update an existing record

Configuration (with specific key):

{
  "record_key": "my-pinned-post"
}

Payload:

"record"

This will update the record at the specified key instead of creating a new one.

⚠️ Authentication Required

Records are published using stored sessions. Users must authenticate through the web interface to create sessions for their DIDs. Without a valid session, publish_record will fail.

Node Type: publish_webhook

Sends HTTP POST requests to external webhooks.

Example: Send notification to Discord

Configuration:

{
  "url": "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL",
  "headers": {
    "Content-Type": "application/json"
  }
}

Payload (data to send):

{
  "val": ["webhook_data"]
}

Input Data:

{
  "webhook_data": {
    "content": "New post detected!",
    "embeds": [{
      "title": "Post Alert",
      "description": "Someone mentioned your site!"
    }]
  }
}

JSONLogic Evaluation

If This Then AT:// uses datalogic-rs, a Rust implementation of JSONLogic, for all data evaluation and transformation. This powerful system allows you to write complex logic in a declarative JSON format.

📚 Resources

Common Operators

The most frequently used operators in blueprints include:

Data Access & Manipulation
  • {"val": ["path", "to", "field"]} - Extract value from nested data
  • {"cat": ["string1", "string2"]} - Concatenate strings
  • {"substr": ["string", start, length]} - Extract substring
  • {"upper": ["text"]} / {"lower": ["text"]} - Change case
  • {"now": []} - Current timestamp
Comparison & Logic
  • {"==": [value1, value2]} - Equality check
  • {"!=": [value1, value2]} - Inequality check
  • {"<": [value1, value2]} / {">": [value1, value2]} - Comparisons
  • {"and": [condition1, condition2]} - Logical AND
  • {"or": [condition1, condition2]} - Logical OR
  • {"!": condition} - Logical NOT
  • {"in": [needle, haystack]} - Check if value in array/string
  • {"starts_with": [string, prefix]} - String prefix check
Array Operations
  • {"some": [array, condition]} - Check if any element matches
  • {"all": [array, condition]} - Check if all elements match
  • {"map": [array, operation]} - Transform each element
  • {"filter": [array, condition]} - Filter elements
  • {"reduce": [array, operation, initial]} - Reduce to single value

Example Jetstream Input Data

When processing Jetstream events, your blueprint receives data in this structure:

Sample Jetstream Post Event

{
  "commit": {
    "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq",
    "collection": "app.bsky.feed.post",
    "operation": "create",
    "record": {
      "$type": "app.bsky.feed.post",
      "createdAt": "2025-09-04T03:19:50.142Z",
      "langs": ["en"],
      "text": "Who's interested in automation?"
    },
    "rev": "3lxy6sv2j5k2b",
    "rkey": "3lxy6suve4c2y"
  },
  "did": "did:plc:tgudj2fjm77pzkuawquqhsxm",
  "kind": "commit",
  "time_us": 1756955990660025
}

Practical JSONLogic Examples

Extract post text

{"val": ["commit", "record", "text"]}

Result: "Who's interested in automation?"

Check if post is in English

{
  "in": ["en", {"val": ["commit", "record", "langs"]}]
}

Result: true

Filter for posts with specific text

{
  "and": [
    {"==": [{"val": ["kind"]}, "commit"]},
    {"==": [{"val": ["commit", "operation"]}, "create"]},
    {"in": ["automation", {"lower": [{"val": ["commit", "record", "text"]}]}]}
  ]
}

Result: true (post contains "automation")

Build AT-URI from components

{
  "cat": [
    "at://",
    {"val": ["did"]},
    "/",
    {"val": ["commit", "collection"]},
    "/",
    {"val": ["commit", "rkey"]}
  ]
}

Result: "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y"

Check for posts with mentions

{
  "some": [
    {"val": ["commit", "record", "facets"]},
    {
      "some": [
        {"val": ["features"]},
        {"==": [{"val": ["$type"]}, "app.bsky.richtext.facet#mention"]}
      ]
    }
  ]
}

Checks if the post has any mention facets

💡 Testing Your Logic

Use the datalogic-rs playground to test your JSONLogic expressions with real data before deploying them in blueprints. Copy the example Jetstream data above and paste it as the input data to experiment with different operators.

Prototypes

Prototypes are templates for blueprints that can be shared with the community.

What are Prototypes?

Prototypes allow you to:

  • Create reusable blueprint templates
  • Define placeholders for customization
  • Share automation patterns with the community
  • Quickly instantiate complex automations

Creating Prototypes

When creating a prototype, you define:

  • Nodes: The blueprint structure with placeholder variables
  • Placeholders: Variables that users fill in when instantiating
    • ID: Variable name (e.g., USER_DID)
    • Type: Data type (did, url, text, number, etc.)
    • Required: Whether the placeholder must be filled
    • Default: Optional default value
    • Validation: Optional regex pattern

Example Placeholder Usage

In your prototype nodes, use placeholders like:

{
  "configuration": {
    "cron": "$[SCHEDULE]"
  },
  "payload": {
    "text": "$[MESSAGE]",
    "did": "$[USER_DID]"
  }
}

Users will be prompted to fill in SCHEDULE, MESSAGE, and USER_DID when creating a blueprint from this prototype.

Browse Prototype Directory → Create a Prototype

XRPC API

XRPC (Cross-Protocol Remote Procedure Call) is the RPC protocol used by AT Protocol applications. If This Then AT:// provides XRPC endpoints for programmatic blueprint management.

🔐 Authentication

All XRPC endpoints require authentication in the form of an inter-service JWT.

Base URL
https://ifthisthen.at/xrpc/

All XRPC methods are prefixed with: tools.graze.ifthisthenat.

getBlueprints

List all blueprints for the authenticated user.

HTTP Request

GET /xrpc/tools.graze.ifthisthenat.getBlueprints
Authorization: Basic {base64(did:password)}

Response (200 OK)

{
  "blueprints": [
    {
      "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123",
      "did": "did:plc:xyz",
      "node_order": ["node1", "node2"],
      "enabled": true,
      "error": null,
      "created_at": "2024-01-01T00:00:00Z"
    }
  ]
}

getBlueprint

Get a specific blueprint with all its nodes.

HTTP Request

GET /xrpc/tools.graze.ifthisthenat.getBlueprint?aturi=at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123
Authorization: Basic {base64(did:password)}

Response (200 OK)

{
  "blueprint": {
    "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123",
    "did": "did:plc:xyz",
    "node_order": ["node1", "node2"],
    "enabled": true,
    "error": null,
    "created_at": "2024-01-01T00:00:00Z"
  },
  "nodes": [
    {
      "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/node1",
      "blueprint": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123",
      "node_type": "jetstream_entry",
      "configuration": {"collection": "app.bsky.feed.post"},
      "payload": true,
      "created_at": "2024-01-01T00:00:00Z"
    }
  ]
}

createBlueprint

Create a new blueprint with nodes. AT-URIs are automatically generated.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.createBlueprint
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "nodes": [
    {
      "node_type": "jetstream_entry",
      "configuration": {
        "collection": ["app.bsky.feed.post"],
        "did": ["did:plc:targetuser"]
      },
      "payload": {"contains": [{"val": ["commit", "record", "text"]}, "hello"]}
    },
    {
      "node_type": "publish_webhook",
      "configuration": {
        "url": "https://webhook.site/unique-url"
      },
      "payload": {"val": []}
    }
  ],
  "enabled": true
}

Response (201 Created)

{
  "blueprint": {
    "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/newid",
    "did": "did:plc:xyz",
    "node_order": ["nodeid1", "nodeid2"],
    "enabled": true,
    "error": null,
    "created_at": "2024-01-01T00:00:00Z"
  },
  "nodes": [
    {
      "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/nodeid1",
      "blueprint": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/newid",
      "node_type": "jetstream_entry",
      "configuration": {"collection": ["app.bsky.feed.post"], "did": ["did:plc:targetuser"]},
      "payload": {"contains": [{"val": ["commit", "record", "text"]}, "hello"]},
      "created_at": "2024-01-01T00:00:00Z"
    },
    {
      "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/nodeid2",
      "blueprint": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/newid",
      "node_type": "publish_webhook",
      "configuration": {"url": "https://webhook.site/unique-url"},
      "payload": {"val": []},
      "created_at": "2024-01-01T00:00:00Z"
    }
  ]
}

updateBlueprint

Update an existing blueprint. This replaces all nodes.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.updateBlueprint
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123",
  "did": "did:plc:xyz",
  "node_order": ["node1", "node2"],
  "nodes": [
    {
      "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/node1",
      "node_type": "periodic_entry",
      "configuration": {"cron": "0 */2 * * *"},
      "payload": true
    },
    {
      "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/node2",
      "node_type": "publish_record",
      "configuration": {},
      "payload": {
        "$type": "app.bsky.feed.post",
        "text": "Automated post every 2 hours",
        "createdAt": {"now": []}
      }
    }
  ],
  "enabled": true
}

Response (200 OK)

{"success": true}

deleteBlueprint

Delete a blueprint and all its nodes.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.deleteBlueprint
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123"
}

Response (200 OK)

{"success": true}

setNode

Create or update a single node in a blueprint.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.setNode
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/node123",
  "blueprint": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123",
  "node_type": "condition",
  "configuration": {},
  "payload": {"==": [{"val": ["status"]}, "active"]}
}

Response (200 OK)

{"success": true}

deleteNode

Delete a single node from a blueprint.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.deleteNode
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.node.definition/node123"
}

Response (200 OK)

{"success": true}

evaluateBlueprint

Manually trigger a blueprint evaluation with custom input data. Useful for testing blueprints or integrating with external services like Zapier.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.evaluateBlueprint
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "aturi": "at://did:plc:xyz/tools.graze.ifthisthenat.blueprint/abc123",
  "payload": {
    "source": "manual",
    "data": {
      "test": "value",
      "timestamp": "2024-01-01T00:00:00Z"
    }
  }
}

Response (202 Accepted)

{
  "success": true,
  "evaluation_id": "550e8400-e29b-41d4-a716-446655440000"
}

Notes

  • Blueprint must be enabled
  • First node must be an entry node (webhook_entry, zap_entry, jetstream_entry, or periodic_entry)
  • Evaluation is queued asynchronously
  • The evaluation_id can be used to track the evaluation

evaluateNode

Test a single node in isolation. Useful for debugging node configurations and payload logic.

HTTP Request

POST /xrpc/tools.graze.ifthisthenat.evaluateNode
Authorization: Basic {base64(did:password)}
Content-Type: application/json

{
  "node_type": "transform",
  "configuration": {},
  "payload": {
    "greeting": {"cat": ["Hello, ", {"val": ["name"]}]},
    "uppercase": {"upper": [{"val": ["text"]}]}
  },
  "input": {
    "name": "World",
    "text": "transform me"
  }
}

Response (200 OK) - Success

{
  "success": true,
  "output": {
    "greeting": "Hello, World",
    "uppercase": "TRANSFORM ME"
  },
  "message": null
}

Response (200 OK) - Filtered

{
  "success": true,
  "output": null,
  "message": "Node evaluation succeeded but filtered out the data"
}

Response (200 OK) - Error

{
  "success": false,
  "output": null,
  "message": "Invalid payload: Transform payload must be an object or array of objects"
}

⚠️ Error Responses

All XRPC endpoints may return these error responses:

  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User not on waitlist or trying to access another user's resources
  • 400 Bad Request: Invalid request parameters or node configuration
  • 404 Not Found: Blueprint or node not found
  • 500 Internal Server Error: Server error during processing

Error Response Format

{
  "error": "ErrorType",
  "message": "Detailed error message"
}
Handle Resolution

For jetstream_entry nodes, the did configuration accepts both DIDs and handles:

  • Handles (e.g., alice.bsky.social) are automatically resolved to DIDs
  • DIDs (e.g., did:plc:xyz) are used as-is
  • Resolution happens during blueprint creation/update
  • Failed handle resolution returns a 400 error