A draft of the documentation for creating if-this-then-at blueprints.
blueprint_creation_guide.md edited
1546 lines 46 kB view raw view rendered
1# Blueprint Creation Guide 2 3This guide explains how to create blueprints for the ifthisthenat automation system. Blueprints are automated workflows that respond to events on the AT Protocol network (including Bluesky) and can perform various actions. 4 5## Overview 6 7A blueprint consists of: 8 91. **Entry Node**: Defines what triggers the blueprint (usually `jetstream_entry` for AT Protocol events) 102. **Processing Nodes**: Transform, filter, or validate the data (e.g., `condition`, `transform`) 113. **Action Node**: Performs the final action (e.g., `publish_record`, `publish_webhook`) 12 13Blueprints are evaluated sequentially, with each node's output becoming the next node's input. 14 15## Jetstream Event Examples 16 17The system receives real-time events from the AT Protocol network via Jetstream. Here are examples of the event structures you'll work with: 18 19### Bluesky Feed Post Event 20 21```json 22{ 23 "commit": { 24 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq", 25 "collection": "app.bsky.feed.post", 26 "operation": "create", 27 "record": { 28 "$type": "app.bsky.feed.post", 29 "createdAt": "2025-09-04T03:19:50.142Z", 30 "langs": [ 31 "en" 32 ], 33 "text": "Who's interested in automation?" 34 }, 35 "rev": "3lxy6sv2j5k2b", 36 "rkey": "3lxy6suve4c2y" 37 }, 38 "did": "did:plc:tgudj2fjm77pzkuawquqhsxm", 39 "kind": "commit", 40 "time_us": 1756955990660025 41} 42``` 43 44### Bluesky Feed Like Event 45 46```json 47{ 48 "commit": { 49 "cid": "bafyreihus6whshnyleuzec4okl7gbweyxfsiacc5qhfnn5wqumpstklvey", 50 "collection": "app.bsky.feed.like", 51 "operation": "create", 52 "record": { 53 "$type": "app.bsky.feed.like", 54 "createdAt": "2025-09-04T03:20:36.439Z", 55 "subject": { 56 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq", 57 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y" 58 } 59 }, 60 "rev": "3lxy6ub5bok2b", 61 "rkey": "3lxy6ub42mk2b" 62 }, 63 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2", 64 "kind": "commit", 65 "time_us": 1756956036754875 66} 67``` 68 69### Bluesky Feed Post Reply Event 70 71```json 72{ 73 "commit": { 74 "cid": "bafyreif53mmglhsbqeuk6zftxh2kauz5l2jbnlavrvluobqdmm33f6uga4", 75 "collection": "app.bsky.feed.post", 76 "operation": "create", 77 "record": { 78 "$type": "app.bsky.feed.post", 79 "createdAt": "2025-09-04T03:26:18.985Z", 80 "langs": [ 81 "en" 82 ], 83 "reply": { 84 "parent": { 85 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq", 86 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y" 87 }, 88 "root": { 89 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq", 90 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y" 91 } 92 }, 93 "text": "I know I sure am." 94 }, 95 "rev": "3lxy76hutsk2b", 96 "rkey": "3lxy76hpwlc2y" 97 }, 98 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2", 99 "kind": "commit", 100 "time_us": 1756956379343219 101} 102``` 103 104### Lexicon Community Calendar Event 105 106```json 107{ 108 "commit": { 109 "cid": "bafyreihenqybbcny2al7t3pi5h5m25jmpqxwfwia5jvntcxh76rhh6wxpe", 110 "collection": "community.lexicon.calendar.event", 111 "operation": "create", 112 "record": { 113 "$type": "community.lexicon.calendar.event", 114 "createdAt": "2025-09-04T03:32:33.621Z", 115 "description": "DAYTON MUSIC FEST 2025 IS SEPTEMBER 5TH AT THE BRIGHTSIDE!\n\nSeptember 5, 2025\nDoors open at 7pm\nIllwin (hip-hop) at 8pm\nSocks (alternative) at 9:15pm\ncrabswithoutlegs (jazz-fusion) at 10:30pm", 116 "locations": [ 117 { 118 "$type": "community.lexicon.location.address", 119 "country": "US", 120 "locality": "Dayton", 121 "name": "The Brightside", 122 "postalCode": "45402", 123 "region": "Ohio", 124 "street": "905 E 3rd St" 125 } 126 ], 127 "mode": "community.lexicon.calendar.event#inperson", 128 "name": "Dayton Music Fest", 129 "startsAt": "2025-09-05T21:30:00.000Z", 130 "status": "community.lexicon.calendar.event#scheduled", 131 "uris": [ 132 { 133 "$type": "community.lexicon.calendar.event#uri", 134 "name": "thebrightsidedayton.com", 135 "uri": "https://www.thebrightsidedayton.com/event-details/dayton-music-fest-2" 136 } 137 ] 138 }, 139 "rev": "3lxy7jnc6wc2b", 140 "rkey": "3lxy7jnbfjs2b" 141 }, 142 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2", 143 "kind": "commit", 144 "time_us": 1756956754949182 145} 146``` 147 148### Lexicon Community Calendar RSVP Event 149 150```json 151{ 152 "commit": { 153 "cid": "bafyreicpfvezre4cuyzvc4xsiri4wodhxworsvfpmkka6o3j3bzkmgdani", 154 "collection": "community.lexicon.calendar.rsvp", 155 "operation": "create", 156 "record": { 157 "$type": "community.lexicon.calendar.rsvp", 158 "createdAt": "2025-09-04T03:32:45.166Z", 159 "status": "community.lexicon.calendar.rsvp#going", 160 "subject": { 161 "cid": "bafyreihenqybbcny2al7t3pi5h5m25jmpqxwfwia5jvntcxh76rhh6wxpe", 162 "uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/community.lexicon.calendar.event/3lxy7jnbfjs2b" 163 } 164 }, 165 "rev": "3lxy7jy3rjc2b", 166 "rkey": "BF1P0SPNSZQPS" 167 }, 168 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2", 169 "kind": "commit", 170 "time_us": 1756956766029684 171} 172``` 173 174## Node Configuration Examples 175 176### Jetstream Entry Nodes 177 178#### Match by Collection 179Triggers when any record in the specified collection is created: 180 181```json 182{ 183 "node_type": "jetstream_entry", 184 "configuration": { 185 "collection": ["app.bsky.feed.post"] 186 }, 187 "payload": true 188} 189``` 190 191#### Match by Identity (DID) 192Triggers when a specific user performs any action: 193 194```json 195{ 196 "node_type": "jetstream_entry", 197 "configuration": { 198 "did": ["did:plc:tgudj2fjm77pzkuawquqhsxm"] 199 }, 200 "payload": true 201} 202``` 203 204#### Match Both Collection and Identity 205Triggers when specific users perform actions in specific collections: 206 207```json 208{ 209 "node_type": "jetstream_entry", 210 "configuration": { 211 "did": ["did:plc:tgudj2fjm77pzkuawquqhsxm", "did:plc:cbkjy5n7bk3ax2wplmtjofq2"], 212 "collection": ["app.bsky.feed.post", "app.bsky.feed.like"] 213 }, 214 "payload": true 215} 216``` 217 218#### Match Posts with Specific Text Content 219Triggers when posts contain specific keywords: 220 221```json 222{ 223 "node_type": "jetstream_entry", 224 "configuration": { 225 "collection": ["app.bsky.feed.post"] 226 }, 227 "payload": { 228 "and": [ 229 {"==": [{"val": ["commit", "operation"]}, "create"]}, 230 {"exists": ["commit", "record", "text"]}, 231 {"or": [ 232 {"starts_with": [{"val": ["commit", "record", "text"]}, "#automation"]}, 233 {"ends_with": [{"val": ["commit", "record", "text"]}, "#ifthisthenat"]} 234 ]} 235 ] 236 } 237} 238``` 239 240#### Match Replies to Specific Post 241Triggers when someone replies to a specific post: 242 243```json 244{ 245 "node_type": "jetstream_entry", 246 "configuration": { 247 "collection": ["app.bsky.feed.post"] 248 }, 249 "payload": { 250 "and": [ 251 {"exists": ["commit", "record", "reply"]}, 252 {"==": [ 253 {"val": ["commit", "record", "reply", "parent", "uri"]}, 254 "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y" 255 ]} 256 ] 257 } 258} 259``` 260 261#### Match Posts with Minimum Length 262Triggers on posts that meet minimum text length requirements: 263 264```json 265{ 266 "node_type": "jetstream_entry", 267 "configuration": { 268 "collection": ["app.bsky.feed.post"] 269 }, 270 "payload": { 271 "and": [ 272 {"exists": ["commit", "record", "text"]}, 273 {">=": [{"length": {"val": ["commit", "record", "text"]}}, 50]}, 274 {"<": [{"length": {"val": ["commit", "record", "text"]}}, 300]} 275 ] 276 } 277} 278``` 279 280### Condition Nodes 281 282#### Check if Like Applies to Specific DIDs 283Ensures a like event is for content from one of several specific users: 284 285```json 286{ 287 "node_type": "condition", 288 "configuration": {}, 289 "payload": { 290 "and": [ 291 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]}, 292 {"exists": ["commit", "record", "subject", "uri"]}, 293 {"in": [ 294 {"slice": [ 295 {"split": [{"val": ["commit", "record", "subject", "uri"]}, "/"]}, 296 1, 297 2 298 ]}, 299 ["did:plc:user1", "did:plc:user2", "did:plc:user3"] 300 ]} 301 ] 302 } 303} 304``` 305 306#### Check Post Language and Length 307Ensures posts are in English and meet length requirements: 308 309```json 310{ 311 "node_type": "condition", 312 "configuration": {}, 313 "payload": { 314 "and": [ 315 {"exists": ["commit", "record", "text"]}, 316 {">=": [{"length": {"val": ["commit", "record", "text"]}}, 10]}, 317 {"<=": [{"length": {"val": ["commit", "record", "text"]}}, 280]}, 318 {"or": [ 319 {"in": ["en", {"val": ["commit", "record", "langs"]}]}, 320 {"!": {"exists": ["commit", "record", "langs"]}} 321 ]} 322 ] 323 } 324} 325``` 326 327#### Check if Event is Recent 328Ensures events are from the last 24 hours: 329 330```json 331{ 332 "node_type": "condition", 333 "configuration": {}, 334 "payload": { 335 ">": [ 336 {"val": ["time_us"]}, 337 {"/": [{"-": [{"*": [{"now": []}, 1000]}, 86400000]}, 1000]} 338 ] 339 } 340} 341``` 342 343#### Validate Calendar Event Location 344Ensures calendar events are in specific cities: 345 346```json 347{ 348 "node_type": "condition", 349 "configuration": {}, 350 "payload": { 351 "and": [ 352 {"==": [{"val": ["commit", "collection"]}, "community.lexicon.calendar.event"]}, 353 {"exists": ["commit", "record", "locations"]}, 354 {">": [{"length": {"val": ["commit", "record", "locations"]}}, 0]}, 355 {"some": [ 356 {"val": ["commit", "record", "locations"]}, 357 {"in": [ 358 {"val": ["locality"]}, 359 ["Dayton", "Columbus", "Cincinnati", "Cleveland"] 360 ]} 361 ]} 362 ] 363 } 364} 365``` 366 367### Webhook Entry Nodes 368 369Webhook entry nodes process HTTP webhook requests, allowing external systems to trigger blueprints via HTTP POST requests to `/webhooks/{blueprint_record_key}`. 370 371#### Basic Webhook Handler 372Processes any valid JSON webhook request: 373 374```json 375{ 376 "node_type": "webhook_entry", 377 "configuration": {}, 378 "payload": true 379} 380``` 381 382#### Filter by Content-Type 383Only processes webhooks with specific content type: 384 385```json 386{ 387 "node_type": "webhook_entry", 388 "configuration": {}, 389 "payload": { 390 "==": [{"val": ["headers", "content-type"]}, "application/json"] 391 } 392} 393``` 394 395#### Validate Webhook Structure 396Ensures webhook contains required fields: 397 398```json 399{ 400 "node_type": "webhook_entry", 401 "configuration": {}, 402 "payload": { 403 "and": [ 404 {"exists": ["body", "event_type"]}, 405 {"exists": ["body", "data"]}, 406 {"in": [{"val": ["body", "event_type"]}, ["user_signup", "payment_success", "order_complete"]]} 407 ] 408 } 409} 410``` 411 412**Webhook Request Structure:** 413```json 414{ 415 "headers": { 416 "content-type": "application/json", 417 "authorization": "Bearer token" 418 }, 419 "body": { 420 "event_type": "user_signup", 421 "data": { /* event data */ } 422 }, 423 "query": { /* URL parameters */ }, 424 "method": "POST", 425 "path": "/webhooks/blueprint-record-key" 426} 427``` 428 429### Periodic Entry Nodes 430 431Periodic entry nodes generate events on a schedule rather than reacting to external events. They use cron expressions to define when they should trigger. 432 433#### Hourly Status Report 434Generates a status report every hour: 435 436```json 437{ 438 "node_type": "periodic_entry", 439 "configuration": { 440 "cron": "0 0 * * * *" // Every hour at minute 0 441 }, 442 "payload": { 443 "event_type": "hourly_status", 444 "timestamp": {"datetime": [{"now": []}]}, 445 "metadata": { 446 "source": "scheduler", 447 "hour": {"format_date": [{"now": []}, "HH"]}, 448 "day_of_week": {"format_date": [{"now": []}, "dddd"]} 449 } 450 } 451} 452``` 453 454#### Daily Summary at 9 AM 455Creates a daily summary post every day at 9 AM: 456 457```json 458{ 459 "node_type": "periodic_entry", 460 "configuration": { 461 "cron": "0 0 9 * * *" // Daily at 9:00 AM 462 }, 463 "payload": { 464 "event_type": "daily_summary", 465 "timestamp": {"datetime": [{"now": []}]}, 466 "summary": { 467 "date": {"format_date": [{"now": []}, "YYYY-MM-DD"]}, 468 "is_weekend": {"in": [ 469 {"format_date": [{"now": []}, "dddd"]}, 470 ["Saturday", "Sunday"] 471 ]} 472 } 473 } 474} 475``` 476 477#### Weekly Monday Morning Report 478Runs every Monday at 8 AM: 479 480```json 481{ 482 "node_type": "periodic_entry", 483 "configuration": { 484 "cron": "0 0 8 * * MON" // Every Monday at 8:00 AM 485 }, 486 "payload": { 487 "event_type": "weekly_report", 488 "week_number": {"format_date": [{"now": []}, "W"]}, 489 "year": {"format_date": [{"now": []}, "YYYY"]}, 490 "timestamp": {"datetime": [{"now": []}]} 491 } 492} 493``` 494 495#### Every 30 Minutes Check 496Performs a check every 30 minutes: 497 498```json 499{ 500 "node_type": "periodic_entry", 501 "configuration": { 502 "cron": "0 */30 * * * *" // Every 30 minutes 503 }, 504 "payload": { 505 "event_type": "periodic_check", 506 "timestamp": {"datetime": [{"now": []}]}, 507 "check_id": {"uuid": []} 508 } 509} 510``` 511 512#### Business Hours Monitoring 513Active only during business hours (9 AM - 5 PM on weekdays): 514 515```json 516{ 517 "node_type": "periodic_entry", 518 "configuration": { 519 "cron": "0 */15 9-17 * * MON-FRI" // Every 15 minutes, 9 AM - 5 PM, Monday-Friday 520 }, 521 "payload": { 522 "event_type": "business_hours_check", 523 "timestamp": {"datetime": [{"now": []}]}, 524 "is_business_hours": true 525 } 526} 527``` 528 529**Important Notes for Periodic Entry:** 530- Cron expressions must have intervals between 30 seconds and 90 days 531- Use 6-field cron format for second-level precision: `SEC MIN HOUR DAY MONTH WEEKDAY` 532- Use 5-field format for minute-level precision: `MIN HOUR DAY MONTH WEEKDAY` 533- Special strings are supported: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly` 534- The payload is evaluated each time the schedule triggers, allowing dynamic content 535 536### Transform Nodes 537 538#### Create Calendar RSVP from Calendar Event 539Automatically RSVP "going" to calendar events: 540 541```json 542{ 543 "node_type": "transform", 544 "configuration": {}, 545 "payload": { 546 "record": { 547 "$type": "community.lexicon.calendar.rsvp", 548 "createdAt": {"datetime": [{"now": []}]}, 549 "status": "community.lexicon.calendar.rsvp#going", 550 "subject": { 551 "cid": {"val": ["commit", "cid"]}, 552 "uri": {"cat": [ 553 "at://", 554 {"val": ["did"]}, 555 "/", 556 {"val": ["commit", "collection"]}, 557 "/", 558 {"val": ["commit", "rkey"]} 559 ]} 560 } 561 } 562 } 563} 564``` 565 566#### Extract Post Text and Author 567Extract specific fields from a Bluesky post for further processing: 568 569```json 570{ 571 "node_type": "transform", 572 "configuration": {}, 573 "payload": { 574 "post_text": {"val": ["commit", "record", "text"]}, 575 "author_did": {"val": ["did"]}, 576 "post_uri": {"cat": [ 577 "at://", 578 {"val": ["did"]}, 579 "/app.bsky.feed.post/", 580 {"val": ["commit", "rkey"]} 581 ]}, 582 "created_time": {"val": ["commit", "record", "createdAt"]}, 583 "has_images": {"exists": ["commit", "record", "embed", "images"]}, 584 "lang": {"first": {"val": ["commit", "record", "langs"]}} 585 } 586} 587``` 588 589#### Create Post with Conditional Content 590Creates different post content based on the type of event: 591 592```json 593{ 594 "node_type": "transform", 595 "configuration": {}, 596 "payload": { 597 "record": { 598 "$type": "app.bsky.feed.post", 599 "createdAt": {"datetime": [{"now": []}]}, 600 "text": {"if": [ 601 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]}, 602 {"cat": [ 603 "Thanks for the like on my post! ❤️" 604 ]}, 605 {"if": [ 606 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.repost"]}, 607 "Thanks for the repost! 🔄", 608 {"cat": [ 609 "New activity: ", 610 {"val": ["commit", "collection"]} 611 ]} 612 ]} 613 ]}, 614 "langs": ["en"] 615 } 616 } 617} 618``` 619 620#### Process Array Data 621Transform array data and calculate aggregates: 622 623```json 624{ 625 "node_type": "transform", 626 "configuration": {}, 627 "payload": { 628 "event_locations": {"map": [ 629 {"val": ["commit", "record", "locations"]}, 630 { 631 "name": {"val": ["name"]}, 632 "city": {"val": ["locality"]}, 633 "full_address": {"cat": [ 634 {"val": ["name"]}, ", ", 635 {"val": ["locality"]}, ", ", 636 {"val": ["region"]} 637 ]} 638 } 639 ]}, 640 "location_count": {"length": {"val": ["commit", "record", "locations"]}}, 641 "has_remote_option": {"some": [ 642 {"val": ["commit", "record", "locations"]}, 643 {"==": [{"val": ["type"]}, "virtual"]} 644 ]} 645 } 646} 647``` 648 649### Publish Record Nodes 650 651#### Publish Auto-Generated Like 652Automatically like posts from specific accounts: 653 654```json 655{ 656 "node_type": "publish_record", 657 "configuration": { 658 "collection": "app.bsky.feed.like", 659 "did": "did:plc:your-did-here" 660 }, 661 "payload": {"val": []} 662} 663``` 664 665#### Publish Reply with Dynamic Content 666Create a reply with content based on the original post: 667 668```json 669{ 670 "node_type": "publish_record", 671 "configuration": { 672 "collection": "app.bsky.feed.post", 673 "did": "did:plc:your-did-here" 674 }, 675 "payload": {"val": []} 676} 677``` 678*Note: The actual record content should come from a preceding transform node.* 679 680#### Publish Calendar RSVP 681Publish an RSVP record to a calendar event: 682 683```json 684{ 685 "node_type": "publish_record", 686 "configuration": { 687 "collection": "community.lexicon.calendar.rsvp", 688 "did": "did:plc:your-did-here", 689 "rkey": {"cat": [ 690 {"val": ["commit", "rkey"]}, 691 "-rsvp" 692 ]} 693 }, 694 "payload": {"val": []} 695} 696``` 697 698### Publish Webhook Nodes 699 700#### Send to Analytics Service 701Send event data to an external analytics service: 702 703```json 704{ 705 "node_type": "publish_webhook", 706 "configuration": { 707 "url": "https://analytics.example.com/events", 708 "timeout_ms": 5000, 709 "headers": { 710 "Authorization": "Bearer your-api-key", 711 "Content-Type": "application/json" 712 } 713 }, 714 "payload": {"val": []} 715} 716``` 717 718#### Send Alert for High-Value Posts 719Send alerts when posts exceed engagement thresholds: 720 721```json 722{ 723 "node_type": "publish_webhook", 724 "configuration": { 725 "url": "https://alerts.example.com/high-engagement", 726 "timeout_ms": 10000, 727 "headers": { 728 "X-Alert-Type": "engagement", 729 "Authorization": "Bearer alert-api-key" 730 } 731 }, 732 "payload": { 733 "alert_type": "high_engagement", 734 "post_uri": {"cat": [ 735 "at://", 736 {"val": ["did"]}, 737 "/app.bsky.feed.post/", 738 {"val": ["commit", "rkey"]} 739 ]}, 740 "metrics": { 741 "likes": {"val": ["metrics", "likeCount"]}, 742 "reposts": {"val": ["metrics", "repostCount"]}, 743 "replies": {"val": ["metrics", "replyCount"]} 744 }, 745 "timestamp": {"datetime": [{"now": []}]} 746 } 747} 748``` 749 750#### Cross-Post to External Platform 751Transform and send data to external social media platforms: 752 753```json 754{ 755 "node_type": "publish_webhook", 756 "configuration": { 757 "url": "https://api.external-platform.com/posts", 758 "timeout_ms": 15000, 759 "headers": { 760 "Authorization": "Bearer external-platform-token", 761 "User-Agent": "ATProto-Bridge/1.0" 762 } 763 }, 764 "payload": { 765 "content": {"val": ["commit", "record", "text"]}, 766 "author": {"val": ["did"]}, 767 "source": "bluesky", 768 "original_uri": {"cat": [ 769 "at://", 770 {"val": ["did"]}, 771 "/app.bsky.feed.post/", 772 {"val": ["commit", "rkey"]} 773 ]} 774 } 775} 776``` 777 778### Facet Text Nodes 779 780Facet text nodes process text to extract mentions (@handles) and URLs, creating AT Protocol facets that enable rich text features like clickable mentions and links. They're typically placed before publish_record nodes to enrich text content. 781 782#### Basic Text Processing 783Process text to extract all mentions and URLs: 784 785```json 786{ 787 "node_type": "facet_text", 788 "configuration": {}, 789 "payload": {} 790} 791``` 792 793#### Custom Field Processing 794Process text from a specific field: 795 796```json 797{ 798 "node_type": "facet_text", 799 "configuration": { 800 "field": "content" 801 }, 802 "payload": {} 803} 804``` 805 806#### Complete Post Creation Pipeline 807Transform data, process facets, and publish a post: 808 809```json 810{ 811 "node_type": "transform", 812 "configuration": {}, 813 "payload": { 814 "text": {"cat": [ 815 "Thanks for following @", 816 {"resolve_handle": {"val": ["did"]}}, 817 "! Check out our website: https://example.com" 818 ]} 819 } 820} 821``` 822 823Followed by: 824 825```json 826{ 827 "node_type": "facet_text", 828 "configuration": { 829 "field": "text" 830 }, 831 "payload": {} 832} 833``` 834 835Then: 836 837```json 838{ 839 "node_type": "transform", 840 "configuration": {}, 841 "payload": { 842 "record": { 843 "$type": "app.bsky.feed.post", 844 "text": {"val": ["text"]}, 845 "facets": {"val": ["facets"]}, 846 "createdAt": {"datetime": [{"now": []}]}, 847 "langs": ["en"] 848 } 849 } 850} 851``` 852 853#### Process Rich Content 854Handle text with multiple mentions and links: 855 856```json 857{ 858 "node_type": "facet_text", 859 "configuration": {}, 860 "payload": {} 861} 862``` 863 864*Input data example:* 865```json 866{ 867 "text": "Great discussion with @alice.bsky.social and @bob.test.com! More info at https://docs.example.com/guide and https://help.site.org" 868} 869``` 870 871*Output structure:* 872```json 873{ 874 "text": "Great discussion with @alice.bsky.social and @bob.test.com! More info at https://docs.example.com/guide and https://help.site.org", 875 "facets": [ 876 { 877 "index": {"byteStart": 21, "byteEnd": 39}, 878 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": "did:plc:alice123"}] 879 }, 880 { 881 "index": {"byteStart": 44, "byteEnd": 57}, 882 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": "did:plc:bob456"}] 883 }, 884 { 885 "index": {"byteStart": 72, "byteEnd": 102}, 886 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": "https://docs.example.com/guide"}] 887 }, 888 { 889 "index": {"byteStart": 107, "byteEnd": 127}, 890 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": "https://help.site.org"}] 891 } 892 ] 893} 894``` 895 896**Important Notes for Facet Text:** 897- Handles are resolved to DIDs using the identity resolver 898- Unresolvable handles are skipped (rendered as plain text) 899- Both mentions and URLs are processed simultaneously 900- Byte positions are calculated accurately for UTF-8 text 901- Output includes both the original text and the facets array 902- Use before publish_record nodes that create posts with rich text 903 904### Debug Action Nodes 905 906Debug action nodes log data as it flows through the blueprint pipeline. They're essential for development, testing, and troubleshooting blueprint behavior. Debug nodes pass data through unchanged, allowing normal pipeline execution to continue. 907 908#### Basic Debug Logging 909Log data at any point in the pipeline: 910 911```json 912{ 913 "node_type": "debug_action", 914 "configuration": {}, 915 "payload": {} 916} 917``` 918 919#### Debug After Filtering 920Inspect what data passes through conditions: 921 922```json 923{ 924 "nodes": [ 925 { 926 "node_type": "jetstream_entry", 927 "configuration": { 928 "collection": ["app.bsky.feed.post"] 929 }, 930 "payload": true 931 }, 932 { 933 "node_type": "condition", 934 "configuration": {}, 935 "payload": { 936 "contains": [{"val": ["commit", "record", "text"]}, "debug"] 937 } 938 }, 939 { 940 "node_type": "debug_action", 941 "configuration": {}, 942 "payload": {} 943 } 944 ] 945} 946``` 947 948#### Debug Transformation Results 949Verify transform node outputs: 950 951```json 952{ 953 "nodes": [ 954 { 955 "node_type": "webhook_entry", 956 "configuration": {}, 957 "payload": true 958 }, 959 { 960 "node_type": "transform", 961 "configuration": {}, 962 "payload": { 963 "processed": true, 964 "original_path": {"val": ["path"]}, 965 "timestamp": {"datetime": [{"now": []}]} 966 } 967 }, 968 { 969 "node_type": "debug_action", 970 "configuration": {}, 971 "payload": {} 972 }, 973 { 974 "node_type": "publish_webhook", 975 "configuration": { 976 "url": "https://example.com/webhook" 977 }, 978 "payload": {"val": []} 979 } 980 ] 981} 982``` 983 984#### Multiple Debug Points 985Trace data flow through complex pipelines: 986 987```json 988{ 989 "nodes": [ 990 { 991 "node_type": "jetstream_entry", 992 "configuration": { 993 "collection": ["app.bsky.feed.like"] 994 }, 995 "payload": true 996 }, 997 { 998 "node_type": "debug_action", 999 "configuration": {}, 1000 "payload": {} 1001 }, 1002 { 1003 "node_type": "transform", 1004 "configuration": {}, 1005 "payload": { 1006 "simplified": {"val": ["commit", "record"]} 1007 } 1008 }, 1009 { 1010 "node_type": "debug_action", 1011 "configuration": {}, 1012 "payload": {} 1013 } 1014 ] 1015} 1016``` 1017 1018**Debug Output Format:** 1019Debug logs include: 1020- Node AT-URI 1021- Blueprint AT-URI 1022- Pretty-printed JSON of the input data 1023 1024**Use Cases for Debug Actions:** 10251. **Development**: Inspect data structure at various pipeline stages 10262. **Testing**: Verify transformations are working correctly 10273. **Troubleshooting**: Identify where data filtering or transformation issues occur 10284. **Validation**: Ensure data matches expected format before actions 10295. **Monitoring**: Log specific events for analysis 1030 1031**Tips:** 1032- Place after transforms to verify output format 1033- Place before conditions to see what data is being evaluated 1034- Place before action nodes to verify final data structure 1035- Use multiple debug nodes to trace data flow through complex pipelines 1036- Remove or comment out debug nodes in production blueprints 1037 1038## Blueprint Structure 1039 1040### Basic Blueprint Flow 1041 10421. **Entry Node**: `jetstream_entry` - Defines the trigger 10432. **Condition Node**: `condition` - Filters events (optional) 10443. **Transform Node**: `transform` - Modifies data (optional) 10454. **Action Node**: `publish_record` or `publish_webhook` - Performs action 1046 1047### Example: Auto-RSVP to Calendar Events 1048 1049```json 1050{ 1051 "nodes": [ 1052 { 1053 "node_type": "jetstream_entry", 1054 "configuration": { 1055 "collection": ["community.lexicon.calendar.event"] 1056 }, 1057 "payload": true 1058 }, 1059 { 1060 "node_type": "condition", 1061 "configuration": {}, 1062 "payload": {"==": [ 1063 {"val": ["commit", "record", "locations", 0, "locality"]}, 1064 "Dayton" 1065 ]} 1066 }, 1067 { 1068 "node_type": "transform", 1069 "configuration": {}, 1070 "payload": { 1071 "record": { 1072 "$type": "community.lexicon.calendar.rsvp", 1073 "createdAt": {"datetime": [{"now": []}]}, 1074 "status": "community.lexicon.calendar.rsvp#going", 1075 "subject": { 1076 "cid": {"val": ["commit", "cid"]}, 1077 "uri": {"cat": [ 1078 "at://", 1079 {"val": ["did"]}, 1080 "/", 1081 {"val": ["commit", "collection"]}, 1082 "/", 1083 {"val": ["commit", "rkey"]} 1084 ]} 1085 } 1086 } 1087 } 1088 }, 1089 { 1090 "node_type": "publish_record", 1091 "configuration": { 1092 "collection": "community.lexicon.calendar.rsvp", 1093 "did": "did:plc:your-did-here" 1094 }, 1095 "payload": {"val": []} 1096 } 1097 ] 1098} 1099``` 1100 1101## Best Practices 1102 1103### Entry Node Configuration 1104 1105- Use specific collections when possible to reduce processing overhead 1106- Use DID filters for user-specific automation 1107- Combine multiple filters for precise targeting 1108 1109### Condition Expressions 1110 1111- Use DataLogic expressions with `{"val": [...]}` to access nested data 1112- Test conditions thoroughly with sample data 1113- Keep expressions simple and readable 1114- Use logical operators like `{"and": [...]}`, `{"or": [...]}` for complex conditions 1115- Remember conditions must evaluate to boolean values 1116 1117### Transform Templates 1118 1119- Use DataLogic expressions for dynamic content generation 1120- Include proper `$type` and `createdAt` fields for AT Protocol records 1121- Wrap record data in a `"record"` field for publish_record nodes 1122- Use `{"now": []}` and `{"datetime": [...]}` for timestamps 1123- Test templates with actual event data to verify correct field access 1124 1125### Security Considerations 1126 1127- Never hardcode sensitive information in blueprints 1128- Use environment variables for API keys and secrets 1129- Validate input data in condition nodes 1130- Be mindful of rate limits when publishing records 1131 1132## Available Node Types 1133 1134- **jetstream_entry**: Entry point for AT Protocol events from Jetstream 1135- **webhook_entry**: Entry point for HTTP webhook requests 1136- **periodic_entry**: Entry point for scheduled/cron-based execution 1137- **condition**: Filter events based on DataLogic expressions 1138- **transform**: Transform data using DataLogic expressions 1139- **facet_text**: Process text to extract mentions, hashtags, and links 1140- **publish_record**: Publish records to AT Protocol repositories 1141- **publish_webhook**: Send HTTP requests to external services 1142- **debug_action**: Log information for debugging purposes 1143 1144**Entry Nodes** (must be first node): `jetstream_entry`, `webhook_entry`, `periodic_entry` 1145**Action Nodes** (blueprint must have at least one): `publish_record`, `publish_webhook`, `debug_action` 1146 1147## Testing Blueprints 1148 11491. Use the debug_action node to log intermediate data 11502. Test with known event structures 11513. Validate generated records against Lexicon schemas 11524. Monitor logs for processing errors 11535. Start with simple blueprints and gradually add complexity 1154 1155## Complex Blueprint Examples 1156 1157### Multi-Stage Content Analysis Pipeline 1158A sophisticated blueprint that analyzes posts for sentiment, extracts mentions, and triggers different actions based on analysis results: 1159 1160```json 1161{ 1162 "nodes": [ 1163 { 1164 "node_type": "jetstream_entry", 1165 "configuration": { 1166 "collection": ["app.bsky.feed.post"], 1167 "did": ["did:plc:specific-user"] 1168 }, 1169 "payload": true 1170 }, 1171 { 1172 "node_type": "condition", 1173 "configuration": {}, 1174 "payload": {"and": [ 1175 {">": [{"length": {"val": ["commit", "record", "text"]}}, 10]}, 1176 {"<": [{"length": {"val": ["commit", "record", "text"]}}, 300]}, 1177 {"not": {"contains": [{"val": ["commit", "record", "text"]}, "RT @"]}} 1178 ]} 1179 }, 1180 { 1181 "node_type": "transform", 1182 "configuration": {}, 1183 "payload": { 1184 "analysis": { 1185 "post_text": {"val": ["commit", "record", "text"]}, 1186 "word_count": {"length": {"split": [{"val": ["commit", "record", "text"]}, " "]}}, 1187 "has_mentions": {"some": [ 1188 {"extract_mentions": {"val": ["commit", "record", "text"]}}, 1189 true 1190 ]}, 1191 "mention_count": {"length": {"extract_mentions": {"val": ["commit", "record", "text"]}}}, 1192 "has_hashtags": {"contains": [{"val": ["commit", "record", "text"]}, "#"]}, 1193 "languages": {"val": ["commit", "record", "langs"]}, 1194 "engagement_score": {"+": [ 1195 {"*": [{"val": ["metrics", "likeCount"]}, 1]}, 1196 {"*": [{"val": ["metrics", "repostCount"]}, 2]}, 1197 {"*": [{"val": ["metrics", "replyCount"]}, 3]} 1198 ]}, 1199 "priority_level": {"if": [ 1200 {">": [{"val": ["metrics", "likeCount"]}, 100]}, 1201 "high", 1202 {"if": [ 1203 {">": [{"val": ["metrics", "likeCount"]}, 10]}, 1204 "medium", 1205 "low" 1206 ]} 1207 ]} 1208 }, 1209 "metadata": { 1210 "processed_at": {"datetime": [{"now": []}]}, 1211 "post_uri": {"cat": [ 1212 "at://", 1213 {"val": ["did"]}, 1214 "/app.bsky.feed.post/", 1215 {"val": ["commit", "rkey"]} 1216 ]}, 1217 "author_handle": {"val": ["handle"]} 1218 } 1219 } 1220 }, 1221 { 1222 "node_type": "condition", 1223 "configuration": {}, 1224 "payload": {"==": [{"val": ["analysis", "priority_level"]}, "high"]} 1225 }, 1226 { 1227 "node_type": "publish_webhook", 1228 "configuration": { 1229 "url": "https://api.analytics.example.com/high-priority-posts", 1230 "timeout_ms": 5000, 1231 "headers": { 1232 "Authorization": "Bearer analytics-key", 1233 "X-Priority": "high" 1234 } 1235 }, 1236 "payload": {"val": []} 1237 } 1238 ] 1239} 1240``` 1241 1242### Dynamic Event Aggregation with Conditional Responses 1243A blueprint that aggregates events by type and responds differently based on patterns: 1244 1245```json 1246{ 1247 "nodes": [ 1248 { 1249 "node_type": "jetstream_entry", 1250 "configuration": { 1251 "collection": [ 1252 "app.bsky.feed.like", 1253 "app.bsky.feed.repost", 1254 "app.bsky.graph.follow" 1255 ] 1256 }, 1257 "payload": true 1258 }, 1259 { 1260 "node_type": "transform", 1261 "configuration": {}, 1262 "payload": { 1263 "event_summary": { 1264 "type": {"val": ["commit", "collection"]}, 1265 "actor": {"val": ["did"]}, 1266 "target": {"if": [ 1267 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]}, 1268 {"val": ["commit", "record", "subject", "uri"]}, 1269 {"if": [ 1270 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.repost"]}, 1271 {"val": ["commit", "record", "subject", "uri"]}, 1272 {"val": ["commit", "record", "subject"]} 1273 ]} 1274 ]}, 1275 "timestamp": {"datetime": [{"now": []}]} 1276 }, 1277 "aggregation_key": {"cat": [ 1278 {"val": ["did"]}, 1279 "-", 1280 {"date_trunc": ["hour", {"now": []}]} 1281 ]}, 1282 "response_action": {"if": [ 1283 {"==": [{"val": ["commit", "collection"]}, "app.bsky.graph.follow"]}, 1284 "send_welcome", 1285 {"if": [ 1286 {"and": [ 1287 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]}, 1288 {">=": [{"count": {"val": ["hourly_likes"]}}, 10]} 1289 ]}, 1290 "send_thanks", 1291 "log_only" 1292 ]} 1293 ]} 1294 } 1295 }, 1296 { 1297 "node_type": "condition", 1298 "configuration": {}, 1299 "payload": {"!=": [{"val": ["response_action"]}, "log_only"]} 1300 }, 1301 { 1302 "node_type": "transform", 1303 "configuration": {}, 1304 "payload": { 1305 "record": { 1306 "$type": "app.bsky.feed.post", 1307 "createdAt": {"datetime": [{"now": []}]}, 1308 "text": {"if": [ 1309 {"==": [{"val": ["response_action"]}, "send_welcome"]}, 1310 {"cat": [ 1311 "Welcome to the community, ", 1312 {"resolve_handle": {"val": ["event_summary", "actor"]}}, 1313 "! Thanks for the follow! 🎉" 1314 ]}, 1315 {"cat": [ 1316 "Thanks for all the engagement today, ", 1317 {"resolve_handle": {"val": ["event_summary", "actor"]}}, 1318 "! Your support means a lot! ❤️" 1319 ]} 1320 ]}, 1321 "langs": ["en"] 1322 } 1323 } 1324 }, 1325 { 1326 "node_type": "publish_record", 1327 "configuration": { 1328 "collection": "app.bsky.feed.post", 1329 "did": "did:plc:your-did-here" 1330 }, 1331 "payload": {"val": []} 1332 } 1333 ] 1334} 1335``` 1336 1337### Advanced Data Processing with Error Handling 1338A blueprint that processes complex nested data structures with fallback logic: 1339 1340```json 1341{ 1342 "nodes": [ 1343 { 1344 "node_type": "jetstream_entry", 1345 "configuration": { 1346 "collection": ["community.lexicon.calendar.event"] 1347 }, 1348 "payload": true 1349 }, 1350 { 1351 "node_type": "transform", 1352 "configuration": {}, 1353 "payload": { 1354 "processed_event": { 1355 "id": {"val": ["commit", "rkey"]}, 1356 "name": {"or": [ 1357 {"val": ["commit", "record", "name"]}, 1358 "Untitled Event" 1359 ]}, 1360 "description": {"or": [ 1361 {"val": ["commit", "record", "description"]}, 1362 "" 1363 ]}, 1364 "start_time": {"or": [ 1365 {"val": ["commit", "record", "startTime"]}, 1366 {"datetime": [{"now": []}]} 1367 ]}, 1368 "locations": {"map": [ 1369 {"or": [ 1370 {"val": ["commit", "record", "locations"]}, 1371 [] 1372 ]}, 1373 { 1374 "name": {"or": [{"val": ["name"]}, "Unknown Location"]}, 1375 "address": {"if": [ 1376 {"exists": ["address"]}, 1377 {"cat": [ 1378 {"or": [{"val": ["address", "streetAddress"]}, ""]}, 1379 {"if": [ 1380 {"and": [ 1381 {"exists": ["address", "streetAddress"]}, 1382 {"exists": ["address", "locality"]} 1383 ]}, 1384 ", ", 1385 "" 1386 ]}, 1387 {"or": [{"val": ["address", "locality"]}, ""]}, 1388 {"if": [ 1389 {"and": [ 1390 {"exists": ["address", "locality"]}, 1391 {"exists": ["address", "region"]} 1392 ]}, 1393 ", ", 1394 "" 1395 ]}, 1396 {"or": [{"val": ["address", "region"]}, ""]} 1397 ]}, 1398 "Address not provided" 1399 ]}, 1400 "coordinates": {"if": [ 1401 {"and": [ 1402 {"exists": ["geo", "latitude"]}, 1403 {"exists": ["geo", "longitude"]} 1404 ]}, 1405 { 1406 "lat": {"val": ["geo", "latitude"]}, 1407 "lng": {"val": ["geo", "longitude"]} 1408 }, 1409 null 1410 ]} 1411 } 1412 ]}, 1413 "attendee_capacity": {"or": [ 1414 {"val": ["commit", "record", "capacity"]}, 1415 -1 1416 ]}, 1417 "is_virtual": {"some": [ 1418 {"or": [ 1419 {"val": ["commit", "record", "locations"]}, 1420 [] 1421 ]}, 1422 {"==": [{"val": ["type"]}, "virtual"]} 1423 ]}, 1424 "tags": {"filter": [ 1425 {"extract_hashtags": {"or": [ 1426 {"val": ["commit", "record", "description"]}, 1427 "" 1428 ]}}, 1429 {"!=": [{"val": []}, ""]} 1430 ]} 1431 } 1432 } 1433 }, 1434 { 1435 "node_type": "condition", 1436 "configuration": {}, 1437 "payload": {"and": [ 1438 {"!=": [{"val": ["processed_event", "name"]}, "Untitled Event"]}, 1439 {">": [{"length": {"val": ["processed_event", "locations"]}}, 0]} 1440 ]} 1441 }, 1442 { 1443 "node_type": "publish_webhook", 1444 "configuration": { 1445 "url": "https://api.eventprocessor.example.com/events", 1446 "timeout_ms": 10000, 1447 "headers": { 1448 "Content-Type": "application/json", 1449 "X-Processor-Version": "2.0" 1450 } 1451 }, 1452 "payload": {"val": []} 1453 } 1454 ] 1455} 1456``` 1457 1458## Common Patterns 1459 1460### Auto-Reply to Mentions 1461```json 1462{ 1463 "entry": "jetstream_entry with mention detection", 1464 "condition": "check if mentioned user matches yours", 1465 "action": "publish_record with reply" 1466} 1467``` 1468 1469### Content Moderation 1470```json 1471{ 1472 "entry": "jetstream_entry for posts", 1473 "condition": "check for inappropriate content", 1474 "action": "publish_webhook to moderation service" 1475} 1476``` 1477 1478### Cross-Platform Posting 1479```json 1480{ 1481 "entry": "jetstream_entry for your posts", 1482 "transform": "convert to external platform format", 1483 "action": "publish_webhook to external API" 1484} 1485``` 1486 1487### Event Aggregation 1488```json 1489{ 1490 "entry": "jetstream_entry for specific collection", 1491 "condition": "filter relevant events", 1492 "action": "publish_webhook to analytics service" 1493} 1494``` 1495 1496### Scheduled Daily Post 1497Create a daily motivational post at 9 AM: 1498```json 1499{ 1500 "nodes": [ 1501 { 1502 "node_type": "periodic_entry", 1503 "configuration": { 1504 "cron": "0 0 9 * * *" // Daily at 9 AM 1505 }, 1506 "payload": { 1507 "event_type": "daily_motivation", 1508 "day": {"format_date": [{"now": []}, "dddd"]}, 1509 "timestamp": {"datetime": [{"now": []}]} 1510 } 1511 }, 1512 { 1513 "node_type": "transform", 1514 "configuration": {}, 1515 "payload": { 1516 "record": { 1517 "$type": "app.bsky.feed.post", 1518 "createdAt": {"datetime": [{"now": []}]}, 1519 "text": {"if": [ 1520 {"==": [{"val": ["day"]}, "Monday"]}, 1521 "Happy Monday! 💪 Let's make this week amazing!", 1522 {"if": [ 1523 {"==": [{"val": ["day"]}, "Friday"]}, 1524 "It's Friday! 🎉 Almost weekend time!", 1525 {"cat": [ 1526 "Good morning! Today is ", 1527 {"val": ["day"]}, 1528 " - make it count! ☀️" 1529 ]} 1530 ]} 1531 ]}, 1532 "langs": ["en"] 1533 } 1534 } 1535 }, 1536 { 1537 "node_type": "publish_record", 1538 "configuration": { 1539 "collection": "app.bsky.feed.post", 1540 "did": "did:plc:your-did-here" 1541 }, 1542 "payload": {"val": []} 1543 } 1544 ] 1545} 1546```