Docs / Automations

Automations as YAML

Everything the editor draws is one node: studio:Automation, in the automations workspace. Put it in your package as content/automations/<slug>/.node.yaml, deploy, and it appears in the editor exactly as if it had been drawn there — same tree, same graph, same inspector. Partners ship automations this way so they are versioned with the rest of the package, reviewed in a pull request and installed on every tenant alike.

The file

# content/automations/renewal-reminder/.node.yaml
node_type: studio:Automation
archetype: studio:AutomationPage
properties:
  title: Renewal reminder
  description: Two weeks before a contract renews, ask the account owner what to do.
  enabled: false                    # ship off; switch on per tenant
  trigger:
    kind: node_event
    workspaces: [contracts]
    node_types: [acme:Contract]
    event_kinds: [Updated]
    paths: ["/active/**"]           # `**` crosses folders; `*` does not
    property_filters:
      status: signed                # matched before any run exists
    ignore_automation_writes: true  # writes by agents/flows/triggers do not re-trigger
  steps:
    - uuid: ask
      element_type: studio:StepHumanTask
      label: Ask the account owner
      assignee_mode: specific
      assignee:
        raisin:ref: /users/internal/marta
        raisin:workspace: raisin:access_control
      task_type: approval
      prompt: |-
        “${steps.__subject.subject.title}” renews on ${steps.__subject.subject.renews_at}.
        Renew it, or let it lapse?
      options:
        - { label: Renew, value: renew }
        - { label: Let it lapse, value: lapse }
      due_in_hours: 72
    - uuid: renew
      element_type: studio:StepFunction
      label: Mark it renewed
      function_ref:
        raisin:ref: /lib/studio/automations/library/update-node
        raisin:workspace: functions
      arguments:
        workspace: ${steps.__subject.workspace}
        path: ${steps.__subject.subject_path}
        properties:
          status: renewed
  edges:
    - id: e_renew
      source: ask
      target: renew
      kind: condition
      condition: steps.ask.action == 'renew'
      label: renew
  entries: [ask]
  exits: [renew, ask]
  layout:
    ask: { x: 200, y: 300 }
    renew: { x: 640, y: 300 }

The parts:

KeyMeaning
title, description, enabledWhat the editor shows. Ship enabled: false and switch it on per tenant, unless the automation is harmless to run everywhere.
triggerOne of the five kinds; the fields per kind are in Steps and connections.
stepsA list of step elements. Each has a uuid (its id — pick a readable one, it is what variables and connections name), an element_type, a label, and the fields of its kind.
edgesThe connections: source and target are step ids; kind is next, condition, error or timeout; a condition edge carries a REL expression, or a label the agent reads when the source step chooses.
entriesWhich steps run first. Several entries run in parallel.
exitsSteps after which the run may end. Include every step that has no outgoing connection, and every step that may be the last to answer.
layoutGraph positions. Optional; the editor tidies a layout it does not have.

Steps, by element type

# Run a function — a library step or your own
- uuid: notify
  element_type: studio:StepFunction
  label: Tell the editor
  function_ref: { raisin:ref: /lib/studio/automations/library/create-task, raisin:workspace: functions }
  arguments: { assignee: /users/internal/marta, title: "Reviewed: ${steps.__subject.subject.title}", task_type: review }

# Ask an agent — plain answer, structured answer, router, tool loop or competition
- uuid: classify
  element_type: studio:StepAgent
  label: Classify
  agent: { raisin:ref: /agents/support-triage, raisin:workspace: functions }
  instruction: |-
    …${steps.read.body}…
  output_schema:                       # "what it must return"
    type: object
    properties:
      category: { type: string, title: Category path }
      priority: { type: string, enum: [low, normal, high, urgent] }
    required: [category, priority]
  # decide: true          → the agent picks one outgoing `condition` edge by its label
  # tools: true           → a tool loop over the agent's tools (max_iterations: 6)
  # compete: true         → competitors: [refs], referee: ref, referee_instruction, max_rounds

# Ask a person
- uuid: review
  element_type: studio:StepHumanTask
  label: Check the reply
  assignee_mode: specific
  assignee: { raisin:ref: /users/internal/marta, raisin:workspace: raisin:access_control }
  task_type: approval                  # action | review | input | approval
  prompt: "…"
  options: [{ label: Send, value: send }, { label: Keep, value: keep }]
  due_in_hours: 24

# Talk it through
- uuid: talk
  element_type: studio:StepChat
  label: Talk it through
  agent: { raisin:ref: /agents/content-reviewer, raisin:workspace: functions }
  participant_mode: specific
  participant: { raisin:ref: /users/internal/marta, raisin:workspace: raisin:access_control }
  system_prompt: "…"
  max_turns: 12
  allow_ai_end: true
  output_schema: { type: object, properties: { title: { type: string } }, required: [title] }

References are always envelopes — { raisin:ref: <path>, raisin:workspace: <ws> } — never bare strings.

Deploy and verify

raisindb package create --check ./my-package      # validates references and YAML
raisindb deploy ./my-package --repo <repo> --install

After a deploy, open the automation in Studio. The compiler ran when the node was written; Problems shows what it found, and the automation’s last compile records the flow it produced. If you change the YAML of an automation that already exists on the tenant, push it as content:

cd my-package && raisindb sync . --push --force --yes --repo <repo> --server <url>

and make sure .raisin-sync.yaml claims /automations/<slug> with mode: replace — otherwise the tenant’s copy is left alone by design.

Testing it

A manual trigger is not required to test: Run now in the editor starts any automation with a subject you choose, and the run is a real run — tasks arrive in the Inbox, agents are called, drafts land in the outbox. For a node-event automation, creating or editing a matching node is the real thing.

The compiled flow is also under test in Studio’s own repository: every shipped automation is compiled by a unit test, so a YAML that no longer compiles fails the build before it reaches a tenant. Do the same in your package: the compiler is a plain module, compileAutomation(properties) returns the flow and a list of problems.

Agents in YAML

An agent is a raisin:AIAgent node in the functions workspace:

# content/functions/agents/support-triage/.node.yaml
node_type: raisin:AIAgent
properties:
  title: Support triage
  provider: maravilla         # a provider configured on the tenant
  model: llama-3.3-70b-full
  temperature: 0.2
  max_tokens: 2048
  execution_context: system
  thinking_enabled: false
  tools:                      # optional — functions the agent may call in a tool loop
    - { raisin:ref: /lib/studio/automations/library/find-nodes, raisin:workspace: functions }
  system_prompt: |-
    You triage a shared support mailbox …

Provider and model are tenant configuration. Ship a sensible default and let the tenant change it on the Agents page; put /functions/agents/<slug> on mode: skip in your .raisin-sync.yaml so a re-sync does not undo that choice.

Pilot documentation. Studio is in a partner pilot. Screens and YAML shown here are current as of the version noted on each page; tell your Maravilla contact when something does not match what you see.