Skip to content

Write a handler

Most deployments use the handler preset. Choose one runtime and place its required entry file at the top level of the deployment directory. To run your own process instead of a handle function, see command deployments.

Create handler.py with a callable named handle.

handler.py
def handle(input: dict):
return {"received": input}

Create handler.ts and export a named synchronous or asynchronous handle function.

handler.ts
import { log, secret } from '@siloga/magmell-agent'
export async function handle(input: Record<string, unknown>) {
log('creating greeting', { name: input.name })
return {
message: `Hello, ${String(input.name ?? 'world')}!`,
hasToken: secret('MODEL_API_KEY') !== undefined,
}
}

@siloga/magmell-agent supplies in-handler helpers and TypeScript types. Declare the exact version reported by the gateway as a devDependency; Magmell supplies its protected runtime copy during the build. See Agent SDK for the helper API and availability details.

package.json
{
"engines": { "node": ">=22 <23" },
"devDependencies": {
"@siloga/magmell-agent": "0.1.0"
}
}

Commit the npm lockfile v3 as package-lock.json. A handler with no third-party packages can omit both package files. Node deployments do not accept Python requirements.

When creation_enabled is true, deploy it explicitly:

Terminal window
magmell deploy ./greeter \
--name greeter \
--version 1 \
--runtime node-22 \
--wait

The handler receives the run’s input object. If a run omits input, the runtime passes an empty dictionary. Return a JSON value: Python uses json.dumps, and Node uses JSON serialization after awaiting the handler. Circular values, BigInt, non-finite numbers, and other non-JSON values fail the run.

def handle(input: dict) -> dict:
numbers = input.get("numbers", [])
return {"sum": sum(numbers), "count": len(numbers)}

An uncaught exception fails the run. Magmell captures its type and message for the run record while protecting secrets from gateway logs.

Add a conventional requirements file beside the handler:

requirements.txt
httpx==0.28.1
pydantic==2.11.7

Then deploy the directory normally:

Terminal window
magmell deploy ./summarizer \
--name summarizer \
--version 1 \
--wait

Pin exact versions for repeatable production builds. Standard-library-only handlers do not need requirements.txt. For system packages and other build commands, see setup and dependencies.

The runtime imports the handler module once when it starts. Put reusable client initialization at module scope and invocation-specific work inside handle.

import httpx
client = httpx.Client(timeout=20)
def handle(input: dict) -> dict:
response = client.get(input["url"])
return {"status": response.status_code}

Do not keep invocation-specific secrets or mutable request state in module globals.

import siloga_agent
def handle(input: dict) -> dict:
siloga_agent.log("starting analysis", document_id=input.get("id"))
with siloga_agent.span("analyze"):
result = analyze(input["text"])
siloga_agent.metric("input.characters", len(input["text"]))
siloga_agent.event("analysis.complete", confidence=result["confidence"])
return result

Events emitted before an exception are retained, which makes them useful for diagnosing failed runs. See events and logs.

The equivalent TypeScript helpers come from @siloga/magmell-agent:

import { event, log, metric, span } from '@siloga/magmell-agent'
export async function handle(input: { text?: string }) {
log('starting analysis')
return span('analyze', async () => {
const result = await analyze(input.text ?? '')
metric('input.characters', (input.text ?? '').length)
event('analysis.complete', { confidence: result.confidence })
return result
})
}
import siloga_agent
def handle(input: dict) -> dict:
api_key = siloga_agent.secret("MODEL_API_KEY")
if not api_key:
raise RuntimeError("MODEL_API_KEY is not configured")
return call_model(api_key, input)

Secrets are scoped to the current invocation. They are not environment variables and should never be returned or included in emitted event data.

On Node.js 22, secret('MODEL_API_KEY') returns string | undefined. The runtime uses AsyncLocalStorage so concurrent asynchronous invocations do not share secret or event context.