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:
| Key | Meaning |
|---|---|
title, description, enabled | What the editor shows. Ship enabled: false and switch it on per tenant, unless the automation is harmless to run everywhere. |
trigger | One of the five kinds; the fields per kind are in Steps and connections. |
steps | A 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. |
edges | The 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. |
entries | Which steps run first. Several entries run in parallel. |
exits | Steps after which the run may end. Include every step that has no outgoing connection, and every step that may be the last to answer. |
layout | Graph 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.