Agent function tooling

Read, search, check and edit a deployed function's source and environment through typed HTTP endpoints built for small, bounded calls.

Docs menu

What a function can call once it runs (db, kv, SQL, fetch, limits, logs) is in the function runtime guide; the typed names are GET /runtime.d.ts.

Each endpoint is an operation, the same one MCP and your functions call. Discover the operation IDs from GET /v1/_operation_registry or the tool manifest at GET /v1/_tools; the routes below are the HTTP form of those operations.

Every endpoint needs an app key, which reaches only its own app.

Changing a function, step by step

Use this sequence when changing a function:

  1. Call GET /v1/_functions/_index for the function name, current content_hash, entry point, enabled state, and deployment counts. GET /v1/_functions/_catalog gives the same bounded summaries grouped by database, so you can discover the whole project without opening any source body.
  2. Call GET /v1/functions/{name} when you need the source body or the complete metadata.
  3. Call POST /v1/_functions/_syntax to validate a proposed source without running it. A response with valid:false holds the line, column, and compiler diagnostic.
  4. Use PATCH /v1/functions/{name}/source for a bounded edit, or PUT /v1/functions/{name}/source for a complete replacement. Always send the content_hash the index returned, as expected_hash.
  5. Treat a 409 as a concurrent update: refresh the index or source and retry against the new hash. Never overwrite a changed function blindly.

For targeted inspection, call GET /v1/functions/{name}/source with start_line and exclusive end_line query fields. It copies only the bounded slice you asked for into the response. max_bytes defaults to a bounded value; lower it yourself if you need to. Send expected_hash when the read must be tied to one specific source.

GET /v1/functions/{name}/search

Pass q to find literal matches without requesting the complete source:

q=checkout&max_matches=32&max_bytes=12000&expected_hash=...

The result holds absolute byte offsets, one-based line numbers, and a bounded line snippet for each match. max_matches defaults to 128 and is capped at 1024; max_bytes defaults to 32 KiB and is capped at 1 MiB. truncated:true means a bound stopped the scan or a snippet copy. The search is literal and line-local: it never runs or parses the function.

Bounded source symbols

GET /v1/functions/{name}/symbols

Call this to locate declaration anchors before reading or editing a range:

max_symbols=128&max_bytes=32768&expected_hash=...

It lists identifiers that follow function, class, const, let, and var, skipping comments and quoted strings, and gives each one's keyword-to-identifier byte span. max_symbols defaults to 128 and is capped at 1024; max_bytes defaults to 32 KiB and is capped at 1 MiB. Treat the result as a navigation hint, not a full JavaScript AST: read the surrounding range before you edit.

For durable history, use the app's branch history: GET /v1/_commits?path=functions/NAME.js (version.log) lists the commits that changed that one function, newest first, and GET /v1/_commits/{version}/unit?path=... (version.unit) reads its source at any of them. Deploying an older source again is a commit of its own, so the history is never rewritten.

Set environment keys independently of source edits: GET /v1/functions/{name}/env reads the current typed key/value map, PUT /v1/functions/{name}/env sets one key, and DELETE /v1/functions/{name}/env/{key} removes one key. An environment write keeps routes, bindings and entry point as they are; it never moves the function's deploy counter, since its content hash does not change. Send the current content_hash as expected_hash when you coordinate with other concurrent changes. Deleting an already absent key still succeeds.

A source edit takes either byte offsets or one-based line ranges. For byte edits, offsets count through the UTF-8 source as Substrio stores it; start is inclusive and end is exclusive. For line edits, start_line is one-based and end_line is exclusive, matching bounded source reads. Send one range form per request: a line range is resolved into the byte range the approval binds. An empty replacement is valid and deletes that range. An inverted, ambiguous, or out-of-bounds range is refused before any write.

Source writes use a preview-then-apply handshake. Call POST /v1/functions/{name}/source/preview with either start/end or start_line/end_line, plus replacement and the current expected_hash. It answers an approval token bound to the base hash, proposed hash, resolved byte range, replacement hash, and expiry. A line-based preview also echoes start_line, end_line, and range_kind:"lines" so you can keep working in your own coordinates. Send that token to PATCH /v1/functions/{name}/source with the same range form; it is resolved again and the approval is spent only once the deployment succeeds. Repeating a preview with the same idempotency_key returns the same approval, and retrying an already applied token returns the same successful deployment result while the proposed hash is still current.

For larger changes, commit on a branch instead of staging: POST /v1/_commits (version.commit) writes the candidate into a branch of the app's history without touching the live function, and POST /v1/_live (version.live.set) makes a version live when the work is ready. A branch holds as many commits as the change needs, and nothing of it is executable until it goes live.

Syntax diagnostics

POST /v1/_functions/_syntax

Request:

{
  "source": "module.exports = (event) => event.value + 1;",
  "source_name": "index.js"
}

source is required. source_name is optional and defaults to agent-source.js. The response is always HTTP 200 for a syntactically checked source:

{"valid":true,"line":0,"column":0,"message":""}

or, for invalid source:

{"valid":false,"line":1,"column":25,"message":"Unexpected token"}

The check only parses the source: it never runs your code or writes anything. Malformed JSON, missing source, a missing app, an authentication failure, and an unavailable runtime all answer the usual Substrio error envelope instead.

Function index

GET /v1/_functions/_index

A deterministic name-sorted index, with no source bodies or environment secret values:

{
  "functions": [
    {
      "name": "welcome",
      "id": "demo/welcome",
      "entry_point": "index.js",
      "content_hash": "bcac97e07a163d9a",
      "size_bytes": 184,
      "enabled": true,
      "route_count": 1,
      "binding_count": 0,
      "environment_key_count": 2
    }
  ],
  "count": 1
}

content_hash is the optimistic-concurrency token for source edits. Call GET /v1/functions/{name} when you need the source text, routes, bindings, timestamps, or the full metadata object. The token is an opaque Substrio deployment identity: compare it for concurrency, never treat it as a cryptographic digest and never recompute it.

GET /v1/_functions/_catalog

A source-free summary of the app's functions, grouped by database: database names, function counts, entry points, hashes, sizes, route and binding counts, and enabled state. max_databases defaults to 64 (cap 256) and max_functions defaults to 4096 (cap 65536); truncated:true means request a larger page. Secret environment values and source bodies are never included.

Bounded source line reads

GET /v1/functions/{name}/source

Query fields:

start_line=120&end_line=146&max_bytes=12000&expected_hash=...

start_line is one-based. end_line is exclusive, so the example returns lines 120 through 145. The server returns line and byte boundaries, the total line count, the current content hash, and the selected UTF-8 source slice:

{
  "function": "welcome",
  "start_line": 120,
  "end_line": 146,
  "line_count": 318,
  "start_offset": 4210,
  "end_offset": 4972,
  "content_hash": "bcac97e07a163d9a",
  "source": "...bounded source slice..."
}

A stale expected_hash, an invalid range, or a response over the byte budget is rejected before source is returned.

Atomic source replacement

PUT /v1/functions/{name}/source

Request fields:

{
  "script": "module.exports = (event) => ({ok: true});",
  "expected_hash": "bcac97e07a163d9a",
  "main": "index.js",
  "routes": ["POST /welcome"],
  "vars": {"MODE": "production"}
}

Only script is required. Leaving out deployment metadata keeps it as the current function has it; sending an array or object replaces that metadata. The same presence rule holds for PATCH /v1/functions/{name} and the POST /v1/deploy compatibility alias: omitting routes, vars or bindings is not destructive, while sending one of them empty clears that metadata. The new source is syntax checked, then deployed as one step. Success returns the normal deployment result with success, function_id, and the complete metadata.

Bounded source editing

POST /v1/functions/{name}/source/preview

Request:

{
  "start_line": 43,
  "end_line": 46,
  "replacement": "2",
  "expected_hash": "bcac97e07a163d9a",
  "idempotency_key": "agent-change-001"
}

The preview checks the complete proposed source without running it, and answers approval_token, base_hash, proposed_hash, original_size, proposed_size, and expires_at. Nothing is deployed yet.

GET /v1/functions/{name}/approvals/{token}

A bounded view of one approval: its app and function, its base/proposed/replacement hashes, byte range, expiry, and state (pending, applied, or expired). The replacement text itself is never returned.

DELETE /v1/functions/{name}/approvals/{token}

Revokes a pending or expired approval. An applied approval is an immutable receipt and returns 409; a revoked token cannot authorize a later edit.

Direct function invocation

POST /v1/functions/{name}/_invoke

Runs the deployed function in the current app. The request body is the JSON event delivered to the function; an omitted body is treated as {}. The optional timeout_ms and memory_mb query parameters override the app's configured CPU and memory limits for this call, and must be positive integers. This endpoint works with no AI integration, and returns the function's JSON result directly. A successful response carries an X-Substrio-Function-Hash header so you can match the run to the source snapshot you inspected.

PATCH /v1/functions/{name}/source

Request:

{
  "start": 43,
  "end": 46,
  "replacement": "2",
  "expected_hash": "bcac97e07a163d9a",
  "approval_token": "..."
}

replacement and approval_token are required, together with either start/end or start_line/end_line; always send expected_hash. The call checks the durable approval and current hash, applies the edit, and persists it with the existing deployment metadata. A token is bound to one exact edit and cannot authorize another range or replacement.

Feedback and recovery

The shared error envelope uses code as the stable discriminator and detail for field-level guidance. Common outcomes are:

Status Meaning Next action
400 Invalid JSON, missing field, invalid range, or syntax error Read detail; correct the request or source. Syntax errors also include valid:false, line, column, and message.
401 Missing or invalid authentication Refresh the app or session credentials.
404 Function name does not exist Refresh the index or deploy the function first.
409 expected_hash does not match Re-read the index/source and retry against the new hash.
503 JavaScript runtime unavailable Retry once the runtime is available.

These operations are intentionally narrow: they do not expose secret values, execute source during validation, or silently merge concurrent edits. The operation registry and generated OpenAPI document remain authoritative for the current schemas and grant requirements.

Function environment

GET /v1/functions/{name}/env

Returns a deterministic environment map and the current source hash:

{
  "function": "welcome",
  "content_hash": "bcac97e07a163d9a",
  "environment": {"MODE": "production", "REGION": "local"},
  "count": 2
}

PUT /v1/functions/{name}/env

Set one key without replacing the rest of the deployment metadata:

{"key":"MODE","value":"production","expected_hash":"bcac97e07a163d9a"}

key and value are required strings. A successful response is the normal deployment result with the updated environment. A stale expected_hash returns 409; an unknown function returns 404.

DELETE /v1/functions/{name}/env/{key}

Remove one key. The optional body may contain expected_hash; deleting a key that is already absent succeeds without writing a new deployment.