Skip to content

Writing Functions

A Function tool runs your code on demand: you write the logic, define the input schema, and Rival runs it when a caller invokes the tool. The Studio code editor supports Python, Python 3.13 — Fast, JavaScript, and Lua. Whichever language you pick, the shape of the code is the same - one file, one handler, the same input and return contract.

For an overview of how code runs on Rival, see Runtime.


The handler pattern

Every Function tool exposes a single entry point named cortexone_handler in a file named cortexone_function.<ext>. These names are required by the runtime - the editor expects them and the platform will not find your code under any other name.

LanguageFilenameEntry point
Pythoncortexone_function.pycortexone_handler(event, context)
JavaScriptcortexone_function.jscortexone_handler(event, context)
Luacortexone_function.luacortexone_handler(event, context)

When a caller invokes your tool, the runtime calls the handler with two arguments: the input payload (event) and an execution context object (context).


Handler signatures

Python

def cortexone_handler(event, context):
# your logic here
return {"statusCode": 200, "body": {"result": "value"}}

JavaScript

function cortexone_handler(event, context) {
// your logic here
return { statusCode: 200, body: { result: "value" } };
}

Lua

function cortexone_handler(event, context)
-- your logic here
return {statusCode = 200, body = {result = "value"}}
end

The event parameter

event is the input payload sent by the caller. It corresponds to the event schema you define in Step 2 of the Tool Editor. When the tool is invoked, the caller provides a JSON object that matches this schema and the runtime passes it to your handler as the event argument.

In Python this arrives as a dictionary, in JavaScript as a plain object, in Lua as a table. Access fields by key and use them in your logic.

If you define multiple events for a single tool, each event has its own named schema. At runtime, the specific event the caller selected determines the shape of the input your handler receives.


The return format

Your handler must always return an object with two fields:

  • statusCode - an integer outcome. Use 200 for success, 400 for bad input, 500 for unexpected failures, and so on. These follow HTTP semantics.
  • body - the response payload. A dictionary, object, or table containing whatever your tool produces. Rival serializes it automatically - you don’t need to convert it to a JSON string.

A response looks like this in each language:

# Python
return {"statusCode": 200, "body": {"summary": "...", "word_count": 142}}
// JavaScript
return { statusCode: 200, body: { summary: "...", wordCount: 142 } };
-- Lua
return {statusCode = 200, body = {summary = "...", word_count = 142}}

If your handler returns None, undefined, or nil, the platform returns a 500 error with a null body. Always return the full {statusCode, body} structure.


Error handling

There are two ways to signal errors from your handler.

Return an intentional error status when the problem is something your code detected - a missing required field, an invalid input value, or a precondition that wasn’t met. This is the preferred pattern for validation errors:

def cortexone_handler(event, context):
if not event.get("text"):
return {"statusCode": 400, "body": {"error": "text field is required"}}
# proceed with logic ...

Let unhandled exceptions propagate for unexpected failures. If your code raises an exception you don’t catch, the platform catches it and returns a 500 response with the error message in the body. This is appropriate for genuinely unexpected conditions - don’t swallow errors that indicate something is broken.


Multiple events

A single tool can expose multiple events, each with its own input schema. For example, a text analysis tool might have one event for summarization and another for keyword extraction. The caller chooses which event to invoke, and the handler receives the corresponding payload.

Inside your handler, you can distinguish between events using a field in the event payload - by convention, a type or action field - and branch your logic accordingly. This lets you keep related functionality in a single tool rather than splitting it across multiple tools.


Testing your handler

Test cases live in the Test panel inside the Code step of the Tool Editor. Provide a sample event payload, run it, and inspect the returned statusCode and body. You can save multiple test cases per tool to cover different inputs and edge cases.

See Testing & Executing for the full Test panel workflow.


No state between runs

Each call to your handler starts fresh: no files persist between runs, global variables do not carry over, and there is no guarantee that two consecutive calls run in the same environment. Write your handler so it depends only on the event input and any external data sources it explicitly fetches (APIs, databases, Digital Assets).

If you need to persist data between calls, write it to an external store - a database, a Digital Asset, or a third-party service. Do not rely on globals or module-level state.


Environment Secrets

Need API keys, database URLs, or other credentials? Store them as Environment Secrets at /user/secrets, attach them to your tool from the Code step, and the platform injects them as environment variables on every run. See Environment Variables for the full workflow.


Next steps

For language-specific details - available libraries, package management, and behavior - see the per-language guides:

For the other tool types in the editor, see MCP and Storm.