{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/sendraven-ai/json-schema/sendraven-ai-create-automation-request-schema.json",
  "title": "CreateAutomationRequest",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/sendraven-ai-api-openapi.json#/components/schemas/CreateAutomationRequest",
  "type": "object",
  "required": [
    "name",
    "slug",
    "identity_id",
    "from",
    "trigger",
    "steps"
  ],
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "slug": {
      "type": "string",
      "pattern": "^[a-z0-9-]+$",
      "description": "Lower-case letters, digits and hyphens; unique in the workspace (409 conflict otherwise). Tags every message the automation sends."
    },
    "identity_id": {
      "type": "string",
      "description": "The id (from GET /v1/domains) of the verified marketing sending domain that from is on. A different id is refused with 422, and the message gives the right one. Trimmed."
    },
    "from": {
      "type": "string",
      "minLength": 3,
      "description": "A bare address or Name <address>, on a domain verified in this workspace as a marketing sending domain, matched exactly on the part after the @. Every step sends as marketing."
    },
    "reply_to": {
      "type": "string",
      "minLength": 3,
      "maxLength": 320,
      "description": "Where replies go instead of from: a founder's drip usually wants answers in their own inbox. The cost is exit_on_reply: SendRaven only receives mail for its own domains, so with a reply-to elsewhere a reply cannot end the sequence. Omit it to keep replies on the sending domain, where they arrive as threads and stop the drip."
    },
    "trigger": {
      "type": "object",
      "required": [
        "kind"
      ],
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "api",
            "contact_added",
            "event",
            "tag_added",
            "date"
          ],
          "description": "date: once per person per date, when a date on the contact in audience_id (the date_attribute) plus offset_days is today in UTC; see date_attribute. api: enrol by POST /v1/automations/{id}/enroll. contact_added: someone new joins audience_id (added one at a time; a batch import only with ?trigger_automations=true). event: POST /v1/events with a name equal to event_name. tag_added: a contact gains tag, from POST /v1/contacts/{id}/tags, a contact added one at a time with it, a dashboard edit, or a batch import with ?trigger_automations=true; a contact that already carried the tag starts nothing, so a sync re-sending every contact's tags is safe. Use tag_added when an audience holds everyone and the tag says who the sequence is for. Any kind can also be enrolled into directly."
        },
        "audience_id": {
          "type": "string",
          "description": "Required for contact_added and date: the audience's id, not its name (a name is refused with 422 and the message gives the id). Trimmed. Ignored for other kinds."
        },
        "date_attribute": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9_]+$",
          "maxLength": 100,
          "description": "Required for date: the contact attribute holding the date, e.g. renewal_date or birthday. Read as YYYY-MM-DD, or an ISO timestamp whose UTC day is used; any other value (05/11/2026, a number, an impossible date) is skipped, never guessed at. Checked once a day by a worker, which also looks back one day, so a date written after that day's check is caught the next day. Activating the automation never backfills: only dates whose day (date plus offset_days) is on or after the day it was activated enrol anyone, and the same holds after a pause. Enrolments go through every rule a manual enrolment does (reenrollment, suppression, topic, tags); a person is enrolled at most once per date."
        },
        "offset_days": {
          "type": "integer",
          "minimum": -365,
          "maximum": 365,
          "default": 0,
          "description": "For date: days from the date to the enrolment. Negative is before it (-7 is a week before a renewal)."
        },
        "recurring_yearly": {
          "type": "boolean",
          "default": false,
          "description": "For date: fire on every anniversary rather than once, for birthdays and yearly renewals. 29 February falls on 28 February in other years. With reenrollment never, only the first year enrols anyone."
        },
        "event_name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "Required for event, e.g. trial_started. 1 to 120 characters, the same limit as POST /v1/events name. Matched exactly, including case. Ignored for other kinds."
        },
        "tag": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60,
          "description": "Required for tag_added, e.g. audio-player. Normalised as contact tags are (lower-cased, spaces become hyphens), so \"Audio Player\" is stored and matched as audio-player. Ignored for other kinds."
        }
      }
    },
    "steps": {
      "type": "array",
      "minItems": 1,
      "maxItems": 20,
      "items": {
        "$ref": "#/$defs/AutomationStepInput"
      }
    },
    "topic_key": {
      "type": "string",
      "minLength": 1,
      "description": "Send every step under this topic. The unsubscribe link in each step then opts the person out of this topic only, and opting out ends the enrolment. Must name an existing topic (GET /v1/topics), or the call answers 422 unknown_topic. Trimmed before it is checked."
    },
    "exit_tags": {
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 60
      },
      "description": "Contact tags that end the sequence. Nobody carrying one is enrolled, and adding one to an enrolled contact ends their enrolment. The way to stop a trial sequence for someone who upgraded: tag them customer. Normalised as contact tags are: trimmed, lower-cased, runs of whitespace become hyphens, deduplicated.",
      "default": []
    },
    "required_tags": {
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 60
      },
      "description": "Contact tags a person must keep. Only people carrying all of them are enrolled, and removing one ends the enrolment. Someone with no contact record has no tags, so is never enrolled. Normalised as exit_tags are.",
      "default": []
    },
    "exit_on_reply": {
      "type": "boolean",
      "description": "End the enrolment when the person replies. Turn off for a sequence that should continue after a reply, such as dunning. Out-of-office replies and bounces never count as a reply.",
      "default": true
    },
    "reenrollment": {
      "type": "string",
      "enum": [
        "after_completion",
        "never"
      ],
      "description": "Who can come back in. after_completion: someone whose enrolment completed or was cancelled can be enrolled again and starts from the first step; only an active enrolment blocks a new one. never: each address goes through the sequence once, and enrolling someone who has already had it answers enrolled false with reason previously_enrolled. Applies to the enrol endpoint, POST /v1/events, contact_added, tag_added and date alike.",
      "default": "after_completion"
    },
    "default_variables": {
      "type": "object",
      "additionalProperties": {
        "type": "string",
        "maxLength": 10000
      },
      "maxProperties": 100,
      "description": "Template values every enrolment gets, under whatever an enrolment passes in variables (which wins for the same name). Read when each step is sent, so a change reaches people already enrolled from their next step. Names are letters, digits, _ and . as in a template placeholder; keys are returned as written. contact_added, tag_added and date enrol with no variables of their own, so for those every placeholder the steps use (their subjects, html and templates) must have a value here, or the call answers 422 missing_variables naming the missing ones.",
      "default": {}
    }
  },
  "$defs": {
    "AutomationStepConditionInput": {
      "type": "object",
      "required": [
        "type"
      ],
      "additionalProperties": false,
      "description": "The one yes/no question this step asks when it comes due. True sends it; false skips it (nothing is sent) and the person moves on to the next step, which waits its own delay from then. Exits are checked first, so a condition never sends to someone an exit removes. has_tag / lacks_tag: the contact carries tag (normalised as contact tags are); someone with no contact record has no tags. engaged / not_engaged: the person opened or clicked any message this enrolment has been sent so far, counting only opens and clicks the scanner check counted, so a security gateway fetching every link is not engagement; not allowed on the first step, which has no earlier message. attribute_equals: the contact attribute key equals value, compared as text (3 equals \"3\"); a missing attribute never matches.",
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "has_tag",
            "lacks_tag",
            "engaged",
            "not_engaged",
            "attribute_equals"
          ]
        },
        "tag": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60,
          "description": "Required for has_tag and lacks_tag."
        },
        "key": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9_]+$",
          "maxLength": 100,
          "description": "Required for attribute_equals: the contact attribute's key."
        },
        "value": {
          "oneOf": [
            {
              "type": "string",
              "maxLength": 1000
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            }
          ],
          "description": "Required for attribute_equals: the value the attribute must equal."
        }
      }
    },
    "AutomationStepInput": {
      "type": "object",
      "required": [
        "delay_minutes",
        "subject"
      ],
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9_-]{1,40}$",
          "description": "The step's stable id. People mid-sequence are kept on a step by its id, so an edit that passes it back keeps them on that step wherever it moves, and a step without one is new. 1 to 40 letters, digits, _ and -. On create it is optional, and generated when left out; two steps may not share one. On PATCH it must be the id of one of the automation's current steps (an unknown id is refused with 422, never taken as a new step), or left out for a new step."
        },
        "condition": {
          "description": "Optional; null or left out sends the step to everyone who reaches it.",
          "anyOf": [
            {
              "allOf": [
                {
                  "$ref": "#/$defs/AutomationStepConditionInput"
                }
              ]
            },
            {
              "type": "null"
            }
          ]
        },
        "delay_minutes": {
          "type": "integer",
          "minimum": 0,
          "maximum": 525600,
          "description": "Minutes to wait: from enrolment for the first step, from the previous step being sent for the rest. 4320 is 3 days; 525600 (365 days) is the maximum."
        },
        "subject": {
          "type": "string",
          "minLength": 1,
          "maxLength": 998,
          "description": "The subject sent for this step, even when template_slug is set. Placeholders are filled from the enrolment's variables."
        },
        "html": {
          "type": "string",
          "description": "Inline body. Give this or template_slug (a step with neither is refused with 422); when both are set the template wins."
        },
        "template_slug": {
          "type": "string",
          "minLength": 1,
          "description": "A stored template's slug (GET /v1/templates), rendered with the enrolment's variables. Must exist now, or the call answers 422 naming the step. Trimmed. Only the template's HTML body is sent."
        }
      }
    }
  }
}