artifuncs.json
Every tool repo has an artifuncs.json at its root. It declares the tool's interface: what type of tool it is, what inputs it takes, what outputs it produces, what settings users can tweak, and which external APIs it's allowed to call.
The file is read by the sandbox to render your tool's UI and validate its inputs and outputs. Invalid JSON blocks saving and publishing.
Minimal example
{
"name": "Sum",
"type": "INPUT_OUTPUT",
"language": "PYTHON",
"fields": {
"input": {
"a": { "type": "number", "default": 0 },
"b": { "type": "number", "default": 0 }
},
"output": {
"sum": { "type": "number" }
}
}
}
Top-level keys
| Key | Required | Description |
|---|---|---|
name | yes | Display name shown on the tool card. |
type | yes | INPUT_OUTPUT or IFRAME. See below. |
language | yes | PYTHON. |
layout | no | HORIZONTAL (default) or VERTICAL. Controls how input and output panels stack. |
fields | yes (for INPUT_OUTPUT) | Input and output field declarations. See Fields. |
settings | no | User-tunable values that persist across runs but aren't part of the per-run input. |
runButton | no | true to show a manual Run button under the form. Hidden by default. See below. |
userInstructions | no | Free-text usage guidance shown to visitors above the tool form. See below. |
meta | no | Public tool metadata — description, seoDescription, tags. Applied when a newer version is approved. See below. |
network | no | Egress allowlist — the exact hosts (and optionally paths) the tool may reach. See below. |
type
INPUT_OUTPUT— The default. Your tool receives aninputobject, returns anoutputobject. Inputs and outputs are rendered as forms from thefieldsblock.IFRAME— Your tool is a self-contained web page rendered in an iframe (URL configured via the General tab of the tool editor).fieldsare ignored.
The type is locked at tool creation and can't be changed.
fields
Two sub-blocks: input and output. Each maps a field name to a field declaration:
"fields": {
"input": {
"source": { "type": "text-field", "label": "Source", "default": "" }
},
"output": {
"result": { "type": "text-field", "label": "Result" }
}
}
The field name (e.g. source) is the key your code reads from input and writes to output. See the Fields reference for every supported type and option.
settings
Same shape as fields.input, but values live in a separate Settings panel and persist between runs. Use settings for things a user tweaks once and reuses (precision, API keys, default formats).
"settings": {
"precision": { "type": "number", "label": "Decimal places", "default": 2 }
}
In your Python code, settings are available as the second argument to process:
def process(input, settings):
return { "sum": round(input["a"] + input["b"], settings["precision"]) }
runButton
Funcs run automatically — an INPUT_OUTPUT func re-runs (debounced) whenever an input
changes, and an OUTPUT func runs on load — so no Run button is shown by default.
Set runButton to true when the user needs an explicit trigger: the func has no input
fields to drive the auto-run, or a run is expensive enough that you'd rather the visitor
ask for it than have every keystroke fire one.
{
"name": "Random Quote",
"type": "OUTPUT",
"language": "PYTHON",
"runButton": true,
"fields": { "input": {}, "output": { "quote": { "type": "text-field" } } }
}
Only the literal boolean true shows the button; false, a missing key, or any other
value hides it. Auto-run behaviour is unchanged either way — the button is an additional
trigger, not a replacement.
userInstructions
Optional free-text guidance for people using your tool. When set, it's shown in a
"How to use" panel above the tool form on the public func page (/f/<handle>).
Use it to explain what the tool expects, how to read the output, or any caveats.
{
"name": "CSV Cleaner",
"type": "INPUT_OUTPUT",
"language": "PYTHON",
"userInstructions": "Upload a CSV with a header row. Empty rows are dropped and columns are trimmed. Large files (>5 MB) may take a few seconds.",
"fields": { "input": { "...": {} }, "output": { "...": {} } }
}
Line breaks in the string are preserved. It's plain text (not Markdown), so write it as you'd want it read.
meta
Public metadata for the tool as a whole — its listing description, SEO/OG description, and
tags. Unlike fields or settings (which belong to a single version), these describe the
tool, so they're applied to the public listing when a version is approved and goes live.
"meta": {
"description": "Count and inspect GPT tokens for any text.",
"seoDescription": "Free online tiktoken tokenizer — token counts and IDs.",
"tags": ["nlp", "tokenizer", "openai"]
}
| Field | Description |
|---|---|
description | Short summary shown on the tool card and page. |
seoDescription | SERP / OG description for the public page. Max 200 characters (longer is truncated). |
tags | List of tag names. New tags are created automatically; existing ones are reused. |
When it's applied. On its own, editing meta and pushing does nothing to the live tool —
it only takes effect when a version is approved (goes from review to published). At that
point, if the approved version is newer than the currently-published one, its meta is
applied to the public tool. Approving an older version (e.g. re-approving v4 while v5 is
already live) leaves the metadata untouched — the newest published version always wins.
Each field is independent: omit one to leave it unchanged. An explicit empty tags: []
clears all tags; omitting tags leaves them as they are.
network
Tools run with no outbound network by default. network.allow declares the exact
endpoints your tool is permitted to reach — declare only what you actually need, so
reviewers (and the people running your tool) can see precisely where it connects.
This includes download_asset() in an
init() hook: fetching a model at
setup time needs the model's host listed here, plus wherever it redirects to (most model
hosts redirect to a CDN).
Note the shape — network is an object with an allow array, not a bare array. A
bare array is ignored, which means deny‑all, and the only symptom is a blocked request at
runtime.
"network": {
"allow": [
"api.stripe.com/v1/charges/**",
"*.googleapis.com/storage/**",
"api.example.com"
]
}
Each entry is host with an optional /path pattern:
- Host — exact (
api.stripe.com) or a*.wildcard that matches subdomains and the bare domain (*.googleapis.commatchesstorage.googleapis.comandgoogleapis.com). Hosts are case-insensitive. - Path (optional) — narrows the entry to matching request paths.
*matches within a single path segment;**matches across segments. Paths are case-sensitive. An entry with no path allows any path on that host.
| Declared | Meaning |
|---|---|
["api.x.com/v1/**"] | only api.x.com, only paths under /v1/ |
["api.x.com"] | api.x.com, any path |
[] (empty) | explicitly no network |
| key omitted | no network (default-deny) |
How it's enforced. The host is enforced at the network layer — a tool physically cannot open a connection to a host that isn't declared, no matter how its code tries (this is the guarantee). The path is declared intent: it's shown to reviewers and checked by the provided HTTP client, but because HTTPS traffic is encrypted, path rules are not enforced at the network layer. Keep secrets safe by scoping to the host you trust; use paths to communicate and narrow intent.
Custom (view) funcs — the same allowlist covers the browser. A custom view
renders inside a sandboxed, isolated iframe, and the same network.allow list
governs what that view's browser-side code (fetch, images, WebSocket, etc.) may
reach — enforced by a Content-Security-Policy on the frame. With nothing declared,
the view has no outbound network at all (default-deny), which is fine for most
views because data flows through ctx.run() / ctx.files (a message to the host,
not a direct request). Declare a host here only if your view fetches from it
directly in the browser. As at the network layer, enforcement is host-granular:
*. wildcards work (*.example.com), but a /path suffix is not enforced for
the view — allowing a host allows any path on it in the browser.
Validation
artifuncs.jsonmust be valid JSON. The IDE blocks save on parse errors.name,type,languageare required.- For
INPUT_OUTPUT,fields.inputandfields.outputmust exist and be objects (they can be empty{}). - Field names must be valid identifiers — letters, digits, and underscores only.
- Field types must be one of the supported types. Unknown types render as an error placeholder.
Publishing runs the same checks plus a deeper review (see Publishing a tool).