Docs / Build your own

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, .deleteNodes. 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 / releaseAtomic 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 read input.workspace and input.path and let subject_defaults fill 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-library plus metadata.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.