Your own functions
The library covers the common steps. When you need your own — open a ticket in a helpdesk, compute a quote, talk to an API with a secret — you write a server function in your package. It runs inside RaisinDB’s function runtime, next to the data, and becomes a step in every automation and a tool for every agent.
The shape
A function is a folder with two files:
content/functions/lib/acme/create-ticket/
├── .node.yaml # the raisin:Function node: name, schemas, limits, policies
└── index.js # export async function handler(input) { … }
# .node.yaml
node_type: raisin:Function
properties:
name: create-ticket # MUST equal the folder name
title: Create a ticket
description: Open a ticket in the helpdesk from a conversation and link it back.
enabled: true
language: javascript
execution_mode: async
entry_file: index.js:handler
version: 1
category: automation-library # offered as a step and as a tool
metadata:
library:
icon: ticket
group: tool
order: 50
verb: ticket
subject_defaults: { workspace: workspace, path: subject_path }
input_schema:
type: object
required: [workspace, path, queue]
properties:
workspace: { title: Workspace, type: string }
path: { title: Conversation, type: string }
queue: { title: Queue, type: string, enum: [billing, technical] }
note: { title: Note for the agent, type: string, format: multiline }
output_schema:
type: object
properties:
success: { type: boolean }
ticket_id: { type: string, title: Ticket }
url: { type: string, title: Link }
resource_limits: { timeout_ms: 30000 }
network_policy: { allow: ["https://helpdesk.example.com/**"] }
// index.js
export async function handler(input = {}) {
const workspace = String(input.workspace ?? '').trim();
const path = String(input.path ?? '').trim();
if (!path.startsWith('/')) throw new Error(`create-ticket: "${path}" is not a node path`);
// Read the node the automation is about.
const node = await raisin.nodes.get(workspace, path);
if (!node) throw new Error(`create-ticket: no node at ${workspace}:${path}`);
const p = node.properties || {};
// Talk to the outside world (allowed by network_policy).
const res = await raisin.http.fetch('https://helpdesk.example.com/api/tickets', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${await raisin.secrets.get('helpdesk_token')}` },
body: JSON.stringify({ subject: p.subject, queue: input.queue, note: input.note }),
});
if (!res.ok) throw new Error(`create-ticket: helpdesk answered ${res.status}`);
const ticket = await res.json();
// Write the answer back where it will be read — never re-derive it later.
await raisin.nodes.update(workspace, path, { properties: { ...p, ticket_id: ticket.id, ticket_url: ticket.url } });
return { success: true, ticket_id: ticket.id, url: ticket.url };
}
Register it in manifest.yaml under functions: and deploy. It appears in the step palette under its group, with an arguments form built from input_schema; its output_schema is what Insert… offers to later steps; and any agent whose tools list names it can call it.
The runtime, briefly
Inside a function, raisin is the platform:
raisin.nodes.get(ws, path), .create(ws, parentPath, { name, node_type, properties }), .update(ws, path, { properties }), .move, .delete | Nodes. update writes the whole property bag it is given — read first, spread, then patch. |
raisin.sql.query(sql, params) | Reads. Cast JSON comparisons: properties->>'status'::String = $1. |
raisin.email.send({ to, subject, text, html, attachments }) | Mail from the platform (needs the email policy grant). |
raisin.http.fetch(url, init) | HTTPS, within the function’s network_policy. |
raisin.secrets.get(name) | Tenant secrets, never in content. |
raisin.tasks.create(request) | An Inbox task. |
raisin.ai.completion(...) | A model call without an agent — for captions, summaries inside a function. |
raisin.inventory.claim / release | Atomic counting (seats, stock). The only safe counter. |
Three rules that save afternoons:
- Throw to fail. A thrown error is what an on error connection catches and what the run records. Returning
{ success: false }is a successful step with a sad payload. - Read the two calling shapes. A trigger wraps the event as
input.flow_input.node; an automation step passes your arguments flat. Library functions readinput.workspaceandinput.pathand letsubject_defaultsfill them. - Never write
__-prefixed properties. They are the engine’s, and on a mounted node your value is silently discarded.
A function as a step vs. a plain trigger
An automation step is right when a person should be able to see, reorder and reuse the call. A plain trigger — a raisin:Trigger node pointing straight at your function — is right for invisible plumbing: keep a denormalised field up to date, stamp a timestamp, mirror a reference into a queryable path.
# content/functions/triggers/on-submission-created/.node.yaml
node_type: raisin:Trigger
properties:
name: on-submission-created # globally unique, not the function's name
title: On submission created
trigger_type: node_event
enabled: true
config: { event_kinds: [Created] }
filters:
workspaces: [submissions]
node_types: [acme:FormSubmission]
function_path: /lib/acme/stamp-submission
The function then reads input.flow_input.node — the whole node after the write — and does its one job. Property filters, paths and node types narrow it before any job is spent, exactly as for an automation’s trigger.
Where it meets automations
- As a step:
category: automation-libraryplusmetadata.library. Subject defaults make the common case zero-configuration. - As a tool: list it under the agent’s
tools; the input schema becomes the tool’s parameters and the description is what the model reads. Write the description for the model: what it does, when to use it, what it returns. - As a guard: a function that decides can return a boolean field and an if connection can read it —
steps.check.allowed == true. - Under test: keep the handler pure enough to call with a plain object. The shipped library functions are the pattern: validate arguments, read, act, read back, return what changed.
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.