Rumatadocs
Browse documentation

AI agent steps

How to add AI behavior with the native agent node: billing, outputs, files and images, callable tools and trusted user scope.

Step-by-step guidance.

Before you start

  • A workflow that will contain the agent step.
  • For configuring agents in Studio, see AI workflow agents.

Use the native agent node

Add AI behavior with a workflow activity of `kind: "agent"`. Do not call a raw model API through an HTTP node. Debug one representative caller-funded run before adding UI, more agents or provider options.

json
{"id":"summarize","kind":"agent","agent":{"systemPrompt":"Summarize the note. Return a summary string."}}

Choose who pays

SituationBilling
Standalone AI endpoint called by authenticated Rumata usersOmit `billing`: the caller’s credits fund the call.
An app with its own users, public triggers or background and scheduled runs`billing: "owner"`: the owner funds the call.
The user explicitly requires an existing stored provider key`{ "secretRef": "OpenRouterKey" }`. Never ask for a key just to pass verification.

Owner billing funds the call; it does not grant app access. AI access affects security, privacy and spend, so authenticate app users before they can trigger it, and ask the user when public versus signed-in is genuinely unclear.

Shape the output

  • Omit `outputMode` for a JSON object, use `"text"` for a string, or `"json_schema"` with `outputSchema` for runtime validation.
  • Enforce exact counts in the schema, for example `minItems` and `maxItems` of 5, rather than relying on prompt wording.
  • When the next node consumes the whole AI output, bind it with `{ "expr": "$" }`.

Assistants that read or change app data

If an assistant must read or change application data, give the agent callable tools for those operations. A preloaded snapshot plus action JSON executed later by the backend does not give the agent those capabilities. The agent must choose and invoke tools, observe their results and only then confirm the outcome.

  • Keep ordinary manual CRUD in the backend; both paths operate on the same records.
  • For bounded actions such as adding or updating a record, use small private child workflows, each with a Run Database Query activity bound to a fixed saved `queryId`. For open-ended questions over a known schema, use the dynamic SQL recipe below instead. Attach them as `workflowTools` of type `subworkflow`, with explicit argument schemas and meaningful descriptions. Use query placeholders such as `{{body.name}}`, and read back every saved graph.
  • A tool must not report success when no row changed. Return not-found and validation outcomes explicitly.

Let an agent query a known database

  1. Discover the SQL Execution Engine activity and its input/config schema. Read the selected factory databaseConnectionId and defaultSchemaName from native context or public get_project_context. Use one-off executeSql only to prepare the requested schema and sample rows.
  2. Attach the SQL activity ID to the agent toolActivityIds. Configure toolConfigs keyed by that ID with factoryId and dbConnectionId bound to the selected factory, and schemaName when required. Omit queryId for dynamic query mode. In a native graph use literal wrappers for toolActivityIds and toolConfigs, and canonical config values inside the tool config.
  3. Describe actual tables and columns in systemPrompt. The agent chooses its SQL query at runtime. Use fixed saved-query child workflows for bounded application actions; use dynamic SQL when the user needs open-ended questions over a known schema.
  4. Run a real question and inspect agent tool-call traces for the selected SQL, successful execution and an answer supported by rows. A configured tool alone is not proof. Enforce authorization in the execution layer; a prompt requesting read-only queries is not enforcement.

Trusted user scope

Authentication and row authorization belong in the tools as well as the backend. Never accept an owner ID, factory ID, connection ID, query ID or credential chosen by the model as authority; system-prompt instructions are not an access-control boundary.

  1. Verify the session in the backend, then call the starter client’s `submit(workflowId, {message}, {applicationAuthorization: request.headers.authorization})`. The service key authorizes the private workflow; the app Bearer session travels separately in private execution headers, including child calls.
  2. Keep the session out of model input, tool arguments, prompts and source.
  3. Configure every protected SQL step with `decodeJWT: true`, the current `factoryId`, and `sessionVariables: [{key:"app.user_id",source:"jwt",value:"id"}]`. Native graph config values need `{literal: ...}` wrappers. Repeat this on each SQL step, and derive inserted ownership the same way.
sql
-- every read, update and delete of owned rows
WHERE owner_id = current_setting('app.user_id')::uuid