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.
- Features may change, break, or be removed without notice
- Blueprints may need to be recreated after updates
- Service availability is not guaranteed
- Data loss may occur during development
- Please report issues and provide feedback to help improve the service
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:
account:email
— Read-only access to your email addressrepo:*
— Create, update, and delete access to every collection and record in your PDSrpc:*
— The ability to create XRPC calls to any XRPC service on your behalf
⚠️ 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
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.
- 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.
did:plc:
- Must be followed by exactly 24 lowercase alphanumeric charactersdid:web:
- Must include valid hostname and optional path segmentsdid: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"
]
}
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 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.)
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
}
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"]
}
- 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:
{}
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"]}
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"
}
]
}
]
}
}
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
URLs are detected with these requirements:
- Must start with
http://
orhttps://
- 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
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"]}
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"]}
- 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
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"]}
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
}
}
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 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
app.bsky.feed.post
- Text posts with optional facetsapp.bsky.feed.like
- Likes on postsapp.bsky.feed.repost
- Reposts of postsapp.bsky.graph.follow
- Follow relationshipsapp.bsky.graph.block
- Block relationships- Custom app-specific record types
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
- Operators Documentation: Complete list of available operators →
- Interactive Playground: Test your JSONLogic expressions →
Common Operators
The most frequently used operators in blueprints include:
{"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
{"==": [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
{"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.
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
- ID: Variable name (e.g.,
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.
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.
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"
}
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