{
  "openapi": "3.1.0",
  "info": {
    "title": "automation API",
    "version": "6.0.0",
    "description": "Unversioned routes for one source → optional queries → AND/OR filters → 1 to 8 ordered actions. No\nmultiple sources or temporal event joins. Configured adapters: cron, incoming, optional price\nsource/query; text_contains, number_compare; webhook and Linux/macOS exec. All sources produce durable\nevents with stable identities; price emits threshold crossings. New rules default to no cooldown. Business routes require a CP-X-API-KEY\nvalidated by cp-auth with automation scope. Tenant identity is the exact Owner string; same-owner keys share\nordinary management, while exec additionally requires a configured Key ID. Other tenants return 404. Only the current rule and event schemas are\nsupported. All request bodies are limited to 64 KiB. Each action retries and records its result\nindependently; failures continue, cancellation stops remaining actions. Singular action is rejected.\n\n类型与用途：\n\n| 类别 | type | 用途与行为 |\n| --- | --- | --- |\n| source | price | 价格阈值来源：条件从不满足变为满足时产生事件，首次报价只建立基线。 |\n| source | cron | 定时来源：五字段 cron，按指定时区、分钟精度产生计划事件，不补发停机期间的事件。 |\n| source | incoming | 入站回调来源：向 POST /hooks/{规则 id} 提交 JSON 对象产生事件，config 必须为 {}。 |\n| query | price | 价格查询：事件发生后读取当前报价，结果存入 queries[id]，自身不产生事件。 |\n| filter | text_contains | 文本条件：检查指定字段是否包含子串，可选择是否区分大小写；不支持正则表达式。 |\n| filter | number_compare | 数值条件：对指定字段作精确十进制比较，支持 gt、gte、lt、lte、eq。 |\n| action | webhook | HTTP 动作：将完整事件 JSON POST 到指定 URL，记录 HTTP 状态，失败时允许重试。 |\n| action | exec | 命令动作：Linux/macOS 使用 Bash 执行命令，事件从 stdin 输入，记录退出码和输出，不自动重试。 |\n\n每条规则只接受一个 source，queries 只补充数据，filters 只判断当前事件上下文，actions 按数组顺序执行。price 来源和查询需要配置 oracle.api_key；exec 只在 Linux/macOS 注册。GET /types 返回当前 Key 可用的类型名称，字段与行为说明见下面各类型 schema。X 监控、X 发帖和邮件类型尚未实现。\n\nAPI 允许任意来源的跨域访问（Access-Control-Allow-Origin: *），不允许浏览器凭据。\nOPTIONS 预检返回 204，允许 GET、POST、PUT、DELETE、OPTIONS，以及 Content-Type、Idempotency-Key 和 CP-X-API-KEY 请求头。\n"
  },
  "security": [
    {
      "APIKey": []
    }
  ],
  "servers": [
    {
      "url": "/"
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "security": [],
        "operationId": "health",
        "summary": "Read process health",
        "description": "Both serve and worker expose this route on their respective ports. Source/worker degradation is HTTP 200 with status=degraded; DB failure is HTTP 503.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Database unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/me": {
      "get": {
        "operationId": "currentIdentity",
        "summary": "Read the authenticated Owner, Key ID and scopes",
        "description": "Key ID is canonical UUID text. No raw API key is returned; responses use Cache-Control no-store.",
        "responses": {
          "200": {
            "description": "Current identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Identity"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/types": {
      "get": {
        "operationId": "types",
        "summary": "List adapter types available to this Key",
        "description": "Price is omitted from both sources and queries when oracle.api_key is empty. X and email are not registered. Exec is returned only for configured allowed Key IDs on Linux/macOS; it requires /bin/bash on the worker. Returns names only, grouped into sources, queries, filters and actions. Purpose, configuration and behavior are documented in the corresponding component schemas and the type overview.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Types"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/triggers": {
      "post": {
        "operationId": "createTrigger",
        "summary": "Create an active rule",
        "requestBody": {
          "required": true,
          "description": "One JSON object, at most 64 KiB. Unknown fields are rejected.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TriggerInput"
              },
              "examples": {
                "incoming_text": {
                  "summary": "Filter incoming text and send a webhook",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "filters": {
                      "type": "text_contains",
                      "config": {
                        "path": "/data/post/text",
                        "text": "bitcoin",
                        "case_sensitive": false
                      }
                    },
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/events"
                        }
                      }
                    ]
                  }
                },
                "weekday_cron": {
                  "summary": "09:00 on weekdays in Tokyo",
                  "value": {
                    "source": {
                      "type": "cron",
                      "config": {
                        "expression": "0 9 * * 1-5",
                        "timezone": "Asia/Tokyo"
                      }
                    },
                    "filters": null,
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/daily"
                        }
                      }
                    ]
                  }
                },
                "price": {
                  "summary": "BTC price alert",
                  "value": {
                    "source": {
                      "type": "price",
                      "config": {
                        "symbol": "BTC",
                        "operator": "gte",
                        "threshold": "61000",
                        "currency": "USD"
                      }
                    },
                    "cooldown_minutes": 30,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/price"
                        }
                      }
                    ]
                  }
                },
                "incoming_price_and_text": {
                  "summary": "Incoming text OR alternatives, AND a queried price condition",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "queries": [
                      {
                        "id": "market",
                        "type": "price",
                        "config": {
                          "symbol": "BTC",
                          "currency": "USD"
                        }
                      }
                    ],
                    "filters": {
                      "and": [
                        {
                          "or": [
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "bitcoin",
                                "case_sensitive": false
                              }
                            },
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "BTC"
                              }
                            }
                          ]
                        },
                        {
                          "type": "number_compare",
                          "config": {
                            "path": "/queries/market/price",
                            "operator": "gt",
                            "value": "61000"
                          }
                        }
                      ]
                    },
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/events"
                        }
                      }
                    ]
                  }
                },
                "incoming_exec": {
                  "summary": "Pass an incoming event to a Bash command",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "actions": [
                      {
                        "type": "exec",
                        "config": {
                          "command": "cat"
                        }
                      }
                    ]
                  }
                },
                "multiple_queries_filters_actions": {
                  "summary": "Two price queries, nested filters, then webhook and Bash actions",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "queries": [
                      {
                        "id": "btc",
                        "type": "price",
                        "config": {
                          "symbol": "BTC",
                          "currency": "USD"
                        }
                      },
                      {
                        "id": "eth",
                        "type": "price",
                        "config": {
                          "symbol": "ETH",
                          "currency": "USD"
                        }
                      }
                    ],
                    "filters": {
                      "and": [
                        {
                          "or": [
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "bitcoin",
                                "case_sensitive": false
                              }
                            },
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "BTC"
                              }
                            }
                          ]
                        },
                        {
                          "type": "number_compare",
                          "config": {
                            "path": "/queries/btc/price",
                            "operator": "gt",
                            "value": "61000"
                          }
                        },
                        {
                          "type": "number_compare",
                          "config": {
                            "path": "/queries/eth/price",
                            "operator": "gt",
                            "value": "3000"
                          }
                        }
                      ]
                    },
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/events"
                        }
                      },
                      {
                        "type": "exec",
                        "config": {
                          "command": "cat"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trigger"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Identical normalized source, queries, filter expression and actions already exist for this Owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Authentication or validation dependency unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listTriggers",
        "summary": "List rules",
        "description": "Only rules belonging to the authenticated Owner. Ordered by created_at descending then id. Unknown query parameters are rejected; no symbol filter.",
        "parameters": [
          {
            "name": "source_type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact source type."
          },
          {
            "name": "action_type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Match rules containing at least one action of this type."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "paused",
                "error"
              ]
            },
            "description": "Deleted rules are never listed."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50
            },
            "description": "Values outside 1..200, or malformed values, use 50."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Negative or malformed values use 0; values are capped at 2147483647."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Trigger"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/triggers/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getTrigger",
        "summary": "Get a rule",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trigger"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "put": {
        "operationId": "replaceTrigger",
        "summary": "Replace a complete definition",
        "description": "Preserves status. Source/query/filter/action/cooldown changes increment revision; note-only changes do not. Source changes reset cooldown and occurrence key. Concurrent PUTs are whole replacements; there is no If-Match precondition. Source changes also reset the durable baseline and activation time; other definition changes preserve source state.",
        "requestBody": {
          "required": true,
          "description": "One JSON object, at most 64 KiB. Unknown fields are rejected.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TriggerInput"
              },
              "examples": {
                "incoming_text": {
                  "summary": "Filter incoming text and send a webhook",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "filters": {
                      "type": "text_contains",
                      "config": {
                        "path": "/data/post/text",
                        "text": "bitcoin",
                        "case_sensitive": false
                      }
                    },
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/events"
                        }
                      }
                    ]
                  }
                },
                "weekday_cron": {
                  "summary": "09:00 on weekdays in Tokyo",
                  "value": {
                    "source": {
                      "type": "cron",
                      "config": {
                        "expression": "0 9 * * 1-5",
                        "timezone": "Asia/Tokyo"
                      }
                    },
                    "filters": null,
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/daily"
                        }
                      }
                    ]
                  }
                },
                "price": {
                  "summary": "BTC price alert",
                  "value": {
                    "source": {
                      "type": "price",
                      "config": {
                        "symbol": "BTC",
                        "operator": "gte",
                        "threshold": "61000",
                        "currency": "USD"
                      }
                    },
                    "cooldown_minutes": 30,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/price"
                        }
                      }
                    ]
                  }
                },
                "incoming_price_and_text": {
                  "summary": "Incoming text OR alternatives, AND a queried price condition",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "queries": [
                      {
                        "id": "market",
                        "type": "price",
                        "config": {
                          "symbol": "BTC",
                          "currency": "USD"
                        }
                      }
                    ],
                    "filters": {
                      "and": [
                        {
                          "or": [
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "bitcoin",
                                "case_sensitive": false
                              }
                            },
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "BTC"
                              }
                            }
                          ]
                        },
                        {
                          "type": "number_compare",
                          "config": {
                            "path": "/queries/market/price",
                            "operator": "gt",
                            "value": "61000"
                          }
                        }
                      ]
                    },
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/events"
                        }
                      }
                    ]
                  }
                },
                "incoming_exec": {
                  "summary": "Pass an incoming event to a Bash command",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "actions": [
                      {
                        "type": "exec",
                        "config": {
                          "command": "cat"
                        }
                      }
                    ]
                  }
                },
                "multiple_queries_filters_actions": {
                  "summary": "Two price queries, nested filters, then webhook and Bash actions",
                  "value": {
                    "source": {
                      "type": "incoming",
                      "config": {}
                    },
                    "queries": [
                      {
                        "id": "btc",
                        "type": "price",
                        "config": {
                          "symbol": "BTC",
                          "currency": "USD"
                        }
                      },
                      {
                        "id": "eth",
                        "type": "price",
                        "config": {
                          "symbol": "ETH",
                          "currency": "USD"
                        }
                      }
                    ],
                    "filters": {
                      "and": [
                        {
                          "or": [
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "bitcoin",
                                "case_sensitive": false
                              }
                            },
                            {
                              "type": "text_contains",
                              "config": {
                                "path": "/data/post/text",
                                "text": "BTC"
                              }
                            }
                          ]
                        },
                        {
                          "type": "number_compare",
                          "config": {
                            "path": "/queries/btc/price",
                            "operator": "gt",
                            "value": "61000"
                          }
                        },
                        {
                          "type": "number_compare",
                          "config": {
                            "path": "/queries/eth/price",
                            "operator": "gt",
                            "value": "3000"
                          }
                        }
                      ]
                    },
                    "cooldown_minutes": 0,
                    "actions": [
                      {
                        "type": "webhook",
                        "config": {
                          "url": "https://example.com/events"
                        }
                      },
                      {
                        "type": "exec",
                        "config": {
                          "command": "cat"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trigger"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Identical normalized source, queries, filter expression and actions already exist for this Owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Authentication or validation dependency unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteTrigger",
        "summary": "Soft-delete a rule",
        "description": "History remains until retention. Pending old-revision events are cancelled by the worker.",
        "responses": {
          "204": {
            "description": "Success"
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/triggers/{id}/pause": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "pauseTrigger",
        "summary": "Pause a rule",
        "description": "Only an actual status change increments revision. Reactivation resets source state and activation time so previous conditions do not act as new threshold crossings.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trigger"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/triggers/{id}/resume": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "resumeTrigger",
        "summary": "Activate a rule",
        "description": "May activate paused or error rules. Exec requires an allowed caller Key ID, which replaces the stored authorization binding even for an already active rule. A changed binding increments revision. Does not replay cancelled events or reset cooldown. Reactivation resets source state and activation time so previous conditions do not act as new threshold crossings.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trigger"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/triggers/{id}/executions": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "listExecutions",
        "summary": "List per-action execution results",
        "description": "Records sorted by completion time descending then record ID; use action_index for configured action order. The limit counts action records, not events. History of owned soft-deleted rules remains readable; an owned rule with no retained history returns [], while an absent or other-owner rule returns 404.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Values outside 1..100, or malformed values, use 20."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Execution"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Rule does not exist or belongs to another Owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/triggers/{id}/events": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "listEvents",
        "summary": "List persisted source events",
        "description": "All sources, including pending, claimed, filtered, cooldown-skipped and cancelled events. Ordered by created_at descending then id; returns [] when there are no retained events. An event ID is shared by all its action execution records. Owned soft-deleted rules remain readable; absent or other-owner rules return 404.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Values outside 1..100, or malformed values, use 20."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SourceEvent"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Rule does not exist or belongs to another Owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/hooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "acceptIncoming",
        "summary": "Accept an incoming event",
        "description": "Only active incoming rules accept events. 202 means saved, not executed. Wrong source, inactive rule or key/data conflict returns 409. Edits to execution semantics cancel pending old-revision events. Capacity exhaustion leaves events pending; cooldown skips events in its window. The caller must own the rule. For exec rules, both the calling Key ID and stored authorization Key ID must be allowed, including idempotent retries.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Optional; at most 200 UTF-8 bytes without surrounding whitespace. Scoped to this rule, valid until receipt retention deletes it. Same key/data returns the original receipt, different data returns 409. Without a key each request creates an event."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "One JSON object up to 64 KiB; becomes event.data without an envelope.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              },
              "example": {
                "post": {
                  "id": "post-123",
                  "text": "Bitcoin release announcement"
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceEvent"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Trigger not visible to this Owner, or price symbol not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate rule or event conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/AuthLimited"
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "triggerFired": {
      "post": {
        "security": [],
        "operationId": "deliverWebhook",
        "summary": "Outgoing webhook action",
        "description": "POST to the configured action URL. The JSON body is the shared source Event, including query results. Each action has a distinct identity event_id:action_index; retries reuse that identity and body. Up to three attempts per webhook, with 1s/5s delays. Non-2xx and transport errors are retryable. Receivers should deduplicate by Idempotency-Key, not the shared body event ID. Claims are not replayed after a process crash.",
        "parameters": [
          {
            "name": "X-Trigger-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Rule UUID."
          },
          {
            "name": "X-Execution-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Action identity: event_id:action_index, with a zero-based index; stable on retries and distinct between actions."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Same action identity as X-Execution-Id. Two actions for one event use different keys even when their URLs are identical."
          },
          {
            "name": "X-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "trigger.fired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted successfully"
          },
          "default": {
            "description": "A non-2xx response triggers retry if attempts remain"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "APIKey": {
        "type": "apiKey",
        "in": "header",
        "name": "CP-X-API-KEY",
        "description": "Remote cp-auth key with automation scope; never pass it in the URL. /healthz and CORS preflight are anonymous."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, ambiguous, invalid or expired API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Missing automation scope or Key ID not allowed to authorize exec (action_forbidden).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthLimited": {
        "description": "Authentication rate or daily quota exceeded; validation requests consume upstream quota.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthUnavailable": {
        "description": "Authentication timeout, network failure or invalid remote identity; business operation is not performed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Identity": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "owner_id",
          "key_id",
          "scopes"
        ],
        "properties": {
          "owner_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Opaque exact Owner string, without surrounding whitespace or the reserved unassigned identity."
          },
          "key_id": {
            "type": "string",
            "format": "uuid"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Spec": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "minLength": 1
          },
          "config": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "PriceSource": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "price"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "symbol",
              "operator",
              "threshold"
            ],
            "properties": {
              "symbol": {
                "type": "string",
                "minLength": 1,
                "maxLength": 20,
                "description": "Trimmed and normalized to uppercase. Must be provided by Oracle."
              },
              "operator": {
                "type": "string",
                "enum": [
                  "gt",
                  "gte",
                  "lt",
                  "lte",
                  "eq"
                ],
                "description": "Comparison of the selected value against the configured threshold/value: gt (>), gte (>=), lt (<), lte (<=), eq (=)."
              },
              "threshold": {
                "type": "string",
                "maxLength": 256,
                "description": "Positive decimal string, including scientific notation. Input and normalized values are limited to 256 bytes; the parsed decimal exponent must be between -256 and 256. Normalized without exponent or redundant zeros."
              },
              "currency": {
                "type": "string",
                "default": "USD",
                "description": "Only USD; omitted or empty means USD."
              }
            }
          }
        },
        "description": "Polls Oracle prices and compares the configured symbol against a positive decimal threshold; currently USD only. Requires oracle.api_key. Emits an event only when the comparison changes from false to true. The first valid quote establishes a baseline without firing, even if already true. The baseline survives process restarts; equal/older and stale quotes do not rearm. Source changes or reactivation reset the baseline. Quote timestamp identifies the crossing; observed transitions during downtime cannot be reconstructed. Event data contains symbol, price, currency and observed_at.",
        "title": "Price threshold source",
        "examples": [
          {
            "type": "price",
            "config": {
              "symbol": "BTC",
              "operator": "lt",
              "threshold": "90000",
              "currency": "USD"
            }
          }
        ]
      },
      "CronSource": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "cron"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "expression"
            ],
            "properties": {
              "expression": {
                "type": "string",
                "description": "Five fields: minute hour day-of-month month day-of-week. No seconds or @every."
              },
              "timezone": {
                "type": "string",
                "default": "UTC",
                "description": "Time zone database name; defaults to UTC."
              }
            }
          }
        },
        "description": "Produces scheduled events from a cron expression in the configured timezone (UTC by default). Five-field schedules produce one event per observed scheduled minute. Excludes occurrences before rule activation. Saves each event and its cursor before filtering/capacity checks. No offline catch-up; already saved pending events survive restart. Event data contains scheduled_at and timezone.",
        "title": "Scheduled time source",
        "examples": [
          {
            "type": "cron",
            "config": {
              "expression": "0 9 * * 1-5",
              "timezone": "Asia/Tokyo"
            }
          }
        ]
      },
      "IncomingSource": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "incoming"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {},
            "description": "Empty object; no source-specific settings. The rule ID determines the callback URL."
          }
        },
        "title": "Incoming HTTP callback source",
        "description": "Produces an event when a JSON object is POSTed to /hooks/{trigger_id} for an active incoming rule. The body becomes Event.data; config must be {}. HTTP 202 confirms persistence, not action completion. An optional Idempotency-Key deduplicates requests within the rule: the same key and JSON data return the original event, different data return 409. Without a key, each request creates a new event. Deduplication keys remain valid until the stored event is deleted by retention.",
        "examples": [
          {
            "type": "incoming",
            "config": {}
          }
        ]
      },
      "Source": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/PriceSource"
          },
          {
            "$ref": "#/components/schemas/CronSource"
          },
          {
            "$ref": "#/components/schemas/IncomingSource"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "price": "#/components/schemas/PriceSource",
            "cron": "#/components/schemas/CronSource",
            "incoming": "#/components/schemas/IncomingSource"
          }
        },
        "description": "The single event-producing adapter for a rule. Choose price for observed threshold crossings, cron for scheduled times, or incoming for HTTP callbacks. Queries and filters do not produce events. Available types are reported by GET /types."
      },
      "TextContainsFilter": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "text_contains"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "text"
            ],
            "properties": {
              "path": {
                "type": "string",
                "default": "",
                "description": "JSON Pointer into the full Event: /data/post/text or /queries/<id>/text. Empty pointer selects the whole event; ~0 and ~1 escape tilde and slash. Missing or non-string values do not match."
              },
              "text": {
                "type": "string",
                "minLength": 1,
                "description": "Nonempty literal substring to find in the selected string."
              },
              "case_sensitive": {
                "type": "boolean",
                "default": true,
                "description": "Whether letter case must match. False compares the lowercase forms of both strings."
              }
            }
          }
        },
        "title": "Text substring filter",
        "description": "Matches when the string selected by config.path contains config.text. Uses literal substring matching, not regular expressions. Matching is case-sensitive by default; case_sensitive=false lowercases both strings before comparison. Missing or non-string fields do not match. Filters only decide whether an existing event proceeds; they do not create events.",
        "examples": [
          {
            "type": "text_contains",
            "config": {
              "path": "/data/text",
              "text": "bitcoin",
              "case_sensitive": false
            }
          }
        ]
      },
      "WebhookAction": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "webhook"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri",
                "pattern": "^https?://",
                "maxLength": 2000,
                "description": "Absolute HTTP(S) URL with a hostname, at most 2000 UTF-8 bytes. No redirects or environment proxy, URL credentials, private/internal or special-purpose destinations. DNS answers are checked at dial time."
              }
            }
          }
        },
        "title": "HTTP webhook action",
        "description": "Sends the complete Event, including query results, as an application/json HTTP POST to config.url. A 2xx response succeeds; other status codes and transport failures allow up to three attempts with 1s/5s delays. Target-policy denials are not retried. Only public Internet addresses are allowed; all DNS answers must pass the address policy, and connections use the checked literal IP. Does not follow redirects or use environment proxies. Output contains http_status when an HTTP response is received. X-Execution-Id and Idempotency-Key are event_id:action_index: stable across retries and distinct for different actions.",
        "examples": [
          {
            "type": "webhook",
            "config": {
              "url": "https://example.com/events"
            }
          }
        ]
      },
      "TriggerInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source",
          "actions"
        ],
        "properties": {
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "queries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PriceQuery"
            },
            "maxItems": 8,
            "default": [],
            "description": "Unique IDs within the rule. Queries run in order against the source event, without access to each other’s results. The entire batch shares execution.timeout. Cooldown skips the event; capacity exhaustion keeps it pending before querying. Query failure leaves events pending and does not consume cooldown."
          },
          "filters": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FilterExpression"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Omitted or null means no condition. Flat arrays are rejected."
          },
          "cooldown_minutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1440,
            "default": 0,
            "description": "Optional cooldown. Events occurring before last_fired_at + cooldown are marked skipped permanently. Zero disables cooldown; event identity, not cooldown, prevents duplicates."
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "default": ""
          },
          "actions": {
            "type": "array",
            "minItems": 1,
            "maxItems": 8,
            "items": {
              "$ref": "#/components/schemas/Action"
            },
            "description": "Required ordered action list. Each receives the same event/query snapshot and runs after the preceding action finishes its retries. Failure continues; rule revision/status changes or shutdown cancel remaining actions. Results are recorded per zero-based action_index. Changing order changes the rule revision and definition identity."
          }
        },
        "description": "Full rule definition with exactly one source. PUT applies the same defaults as POST; omitted queries become [] and filters become null. Duplicate normalized source/queries/filters/actions definitions are rejected; cooldown and note do not participate in uniqueness. Array order participates; logical equivalence is not inferred."
      },
      "Trigger": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source",
          "queries",
          "filters",
          "actions",
          "cooldown_minutes",
          "note",
          "id",
          "revision",
          "status",
          "last_fired_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "queries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PriceQuery"
            },
            "maxItems": 8,
            "default": [],
            "description": "Unique IDs within the rule. Queries run in order against the source event, without access to each other’s results. The entire batch shares execution.timeout. Cooldown skips the event; capacity exhaustion keeps it pending before querying. Query failure leaves events pending and does not consume cooldown."
          },
          "filters": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FilterExpression"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Omitted or null means no condition. Flat arrays are rejected."
          },
          "cooldown_minutes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1440,
            "default": 0,
            "description": "Optional cooldown. Events occurring before last_fired_at + cooldown are marked skipped permanently. Zero disables cooldown; event identity, not cooldown, prevents duplicates."
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "default": ""
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "revision": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "error"
            ]
          },
          "last_fired_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "actions": {
            "type": "array",
            "minItems": 1,
            "maxItems": 8,
            "items": {
              "$ref": "#/components/schemas/Action"
            },
            "description": "Required ordered action list. Each receives the same event/query snapshot and runs after the preceding action finishes its retries. Failure continues; rule revision/status changes or shutdown cancel remaining actions. Results are recorded per zero-based action_index. Changing order changes the rule revision and definition identity."
          }
        }
      },
      "Event": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "trigger_id",
          "revision",
          "source",
          "occurred_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Persisted source event ID, unchanged across polling, queries and action retries. Duplicate source keys reuse the existing event; retention limits external-key deduplication."
          },
          "type": {
            "type": "string",
            "const": "trigger.fired"
          },
          "trigger_id": {
            "type": "string",
            "format": "uuid"
          },
          "revision": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "source": {
            "$ref": "#/components/schemas/Spec"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {},
          "queries": {
            "type": "object",
            "additionalProperties": true,
            "description": "Query outputs keyed by query ID. Omitted without queries. The exact data and query results are preserved across action retries."
          }
        }
      },
      "Execution": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "trigger_id",
          "event_id",
          "trigger_revision",
          "source_type",
          "action_type",
          "input",
          "output",
          "status",
          "attempts",
          "error",
          "at",
          "action_index"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "trigger_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_id": {
            "type": "string",
            "description": "Source event ID shared by every action in this run."
          },
          "trigger_revision": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "source_type": {
            "type": "string"
          },
          "action_type": {
            "type": "string"
          },
          "input": {
            "$ref": "#/components/schemas/Event"
          },
          "output": {},
          "status": {
            "type": "string",
            "enum": [
              "succeeded",
              "failed",
              "cancelled"
            ]
          },
          "attempts": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "action_index": {
            "type": "integer",
            "minimum": 0,
            "maximum": 7,
            "description": "Zero-based position in actions at trigger_revision. The pair (event_id, action_index) uniquely identifies this action execution."
          }
        },
        "description": "One final record per event/action_index, including all attempts for that action. Not-started actions cancelled by a rule change or shutdown have attempts=0. This is not an aggregate event result. A crash or storage failure can leave missing records."
      },
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code",
          "error"
        ],
        "properties": {
          "code": {
            "type": "string",
            "examples": [
              "invalid_request",
              "unknown_source_type",
              "unknown_query_type",
              "unknown_filter_type",
              "unknown_action_type",
              "unknown_symbol",
              "not_found",
              "duplicate_trigger",
              "event_conflict",
              "unauthorized",
              "forbidden",
              "action_forbidden",
              "auth_rate_limited",
              "auth_quota_exceeded",
              "auth_unavailable",
              "dependency_unavailable",
              "internal"
            ]
          },
          "error": {
            "type": "string"
          }
        }
      },
      "Types": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "sources",
          "queries",
          "filters",
          "actions"
        ],
        "properties": {
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "filters": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "actions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "queries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "WorkerHealth": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "state",
          "in_flight"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "last_cycle_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_success_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_error": {
            "type": "string"
          },
          "delivery_error": {
            "type": "string"
          },
          "in_flight": {
            "type": "integer",
            "minimum": 0,
            "description": "Reserved slots covering queries, filters and action attempts, including retry waits."
          }
        }
      },
      "Health": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "status",
          "db",
          "sources",
          "time"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "db": {
            "type": "string",
            "enum": [
              "ok",
              "down"
            ]
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "ok",
                "degraded"
              ]
            }
          },
          "worker": {
            "$ref": "#/components/schemas/WorkerHealth"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PriceQuery": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "config"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$",
            "description": "Unique query name within this rule; selects the key in Event.queries."
          },
          "type": {
            "type": "string",
            "const": "price"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "symbol"
            ],
            "properties": {
              "symbol": {
                "type": "string",
                "minLength": 1,
                "maxLength": 20,
                "description": "Trimmed and normalized to uppercase. Must be provided by Oracle."
              },
              "currency": {
                "type": "string",
                "default": "USD",
                "description": "Only USD; omitted or empty means USD."
              }
            }
          }
        },
        "description": "Reads a current Oracle quote after the source event. Returns symbol, price (decimal string), currency and observed_at. Missing, future-dated or stale quotes fail the query. Does not initiate a run. Requires oracle.api_key. The result is stored in Event.queries[id] and filters can read /queries/<id>/price. Actions share this query snapshot, including on retries.",
        "title": "Current price query",
        "examples": [
          {
            "id": "market",
            "type": "price",
            "config": {
              "symbol": "BTC",
              "currency": "USD"
            }
          }
        ]
      },
      "NumberCompareFilter": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "number_compare"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "operator",
              "value"
            ],
            "properties": {
              "path": {
                "type": "string",
                "default": "",
                "description": "JSON Pointer into the full Event, e.g. /queries/market/price."
              },
              "operator": {
                "type": "string",
                "enum": [
                  "gt",
                  "gte",
                  "lt",
                  "lte",
                  "eq"
                ],
                "description": "Comparison of the selected value against the configured threshold/value: gt (>), gte (>=), lt (<), lte (<=), eq (=)."
              },
              "value": {
                "type": "string",
                "maxLength": 256,
                "description": "Exact decimal string, including scientific notation. Input and normalized values must fit 256 UTF-8 bytes; parsed decimal exponent must be within [-256,256]. Normalized without exponent or redundant zeros."
              }
            }
          }
        },
        "description": "Compares a decimal string or JSON number without floating-point conversion. Missing, null, non-numeric or out-of-bounds input does not match. Reads config.path from the full Event and compares that value (left operand) with config.value (right operand). Evaluates one event context; it does not monitor threshold crossings or produce new events.",
        "title": "Decimal comparison filter",
        "examples": [
          {
            "type": "number_compare",
            "config": {
              "path": "/queries/market/price",
              "operator": "gt",
              "value": "90000"
            }
          }
        ]
      },
      "FilterExpression": {
        "description": "One filter leaf or an AND/OR group. Groups evaluate in order and short-circuit. At most 16 leaves and depth 8 (root depth 1); enforced by the server across the whole tree. Reads one Event including query results, never waits for other source events.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/TextContainsFilter"
          },
          {
            "$ref": "#/components/schemas/NumberCompareFilter"
          },
          {
            "$ref": "#/components/schemas/AndExpression"
          },
          {
            "$ref": "#/components/schemas/OrExpression"
          }
        ]
      },
      "AndExpression": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "and"
        ],
        "properties": {
          "and": {
            "type": "array",
            "minItems": 2,
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/FilterExpression"
            }
          }
        }
      },
      "OrExpression": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "or"
        ],
        "properties": {
          "or": {
            "type": "array",
            "minItems": 2,
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/FilterExpression"
            }
          }
        }
      },
      "ExecAction": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "config"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "exec"
          },
          "config": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "command"
            ],
            "properties": {
              "command": {
                "type": "string",
                "minLength": 1,
                "maxLength": 16384,
                "description": "Nonblank Bash command, at most 16 KiB UTF-8, without NUL. Executed by /bin/bash --noprofile --norc -c; no command evaluation at validation time. Event JSON is passed on stdin. Inherits worker identity, working directory and environment."
              }
            }
          }
        },
        "description": "Runs config.command through /bin/bash --noprofile --norc -c on the worker. Passes the complete Event JSON, including query results, on stdin rather than interpolating it into the command. Linux and macOS only. Zero exit status succeeds. Nonzero exit, launch failure, timeout and cancellation are not automatically retried. execution.timeout applies; pipe cleanup adds at most one second. Termination kills the command process group; commands must run in the foreground. Output contains exit_code (-1 when signalled or not started), stdout, stderr, stdout_truncated and stderr_truncated. Each stream captures at most 64 KiB of raw bytes; subsequent bytes are drained and discarded. NUL bytes and invalid UTF-8 are represented as U+FFFD in stdout/stderr text so the output can be stored in JSONB. Text conversion may increase the byte length. Literal backslash escapes are preserved.",
        "title": "Bash command action",
        "examples": [
          {
            "type": "exec",
            "config": {
              "command": "cat"
            }
          }
        ]
      },
      "Action": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/WebhookAction"
          },
          {
            "$ref": "#/components/schemas/ExecAction"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "webhook": "#/components/schemas/WebhookAction",
            "exec": "#/components/schemas/ExecAction"
          }
        },
        "description": "One operation in the ordered actions array. Choose webhook to send an HTTP POST or exec to run Bash on the worker. Each action receives the same event and query snapshot, with its own retry decision, idempotency identity and execution record."
      },
      "SourceEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "trigger_id",
          "revision",
          "data",
          "status",
          "created_at",
          "source_type",
          "occurred_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "trigger_id": {
            "type": "string",
            "format": "uuid"
          },
          "revision": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "claimed",
              "filtered",
              "skipped",
              "cancelled"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "source_type": {
            "type": "string",
            "description": "Source type at the time the event was saved."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "Source occurrence time; incoming events use acceptance time. Distinct from storage time created_at."
          }
        },
        "description": "Durable event from any source. Filtered and cooldown-skipped events are terminal. Claimed is not proof of action success; read executions for the result. Unclaimed events from obsolete revisions are cancelled. Claiming is not crash-replayable, and downstream side effects are not exactly-once."
      }
    }
  }
}
