/docs/Development/Artifuncs Json
back to app →

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

▸json
{
  "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

KeyRequiredDescription
nameyesDisplay name shown on the tool card.
typeyesINPUT_OUTPUT or IFRAME. See below.
languageyesPYTHON.
layoutnoHORIZONTAL (default) or VERTICAL. Controls how input and output panels stack.
fieldsyes (for INPUT_OUTPUT)Input and output field declarations. See Fields.
settingsnoUser-tunable values that persist across runs but aren't part of the per-run input.
runButtonnotrue to show a manual Run button under the form. Hidden by default. See below.
userInstructionsnoFree-text usage guidance shown to visitors above the tool form. See below.
metanoPublic tool metadata — description, seoDescription, tags. Applied when a newer version is approved. See below.
networknoEgress allowlist — the exact hosts (and optionally paths) the tool may reach. See below.
scopesnoUser-data scopes the tool requests (e.g. user.ip_address), each with a required reason shown to users on the consent screen. See below.
integrationsnoThird-party apps the tool calls through the user's connected accounts via api('<app>'), each with the provider OAuth scopes it needs and a required reason. See below.

type

  • INPUT_OUTPUT — The default. Your tool receives an input object, returns an output object. Inputs and outputs are rendered as forms from the fields block.
  • IFRAME — Your tool is a self-contained web page rendered in an iframe (URL configured via the General tab of the tool editor). fields are 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:

▸json
"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).

▸json
"settings": {
  "precision": { "type": "number", "label": "Decimal places", "default": 2 }
}

In your Python code, settings are available as the second argument to process:

▸python
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.

▸json
{
  "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.

▸json
{
  "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.

▸json
"meta": {
  "description": "Count and inspect GPT tokens for any text.",
  "seoDescription": "Free online tiktoken tokenizer — token counts and IDs.",
  "tags": ["nlp", "tokenizer", "openai"]
}
FieldDescription
descriptionShort summary shown on the tool card and page.
seoDescriptionSERP / OG description for the public page. Max 200 characters (longer is truncated).
tagsList 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.

▸json
"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.com matches storage.googleapis.com and googleapis.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.
DeclaredMeaning
["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 omittedno 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.

scopes

Some tools need data about the person running them. That data is never available by default: declare a scope with a reason, and the platform shows the user a consent screen (when adding the tool to a board, before the first run on the public page, and again when a new version requests scopes the user hasn't approved). The run is rejected by the sandbox until every declared scope is approved — approval is all-or-nothing.

▸json
"scopes": {
  "user.ip_address": "Detects your country to pick the right tax rate"
}

The reason string is required and user-facing — it is exactly the text shown on the consent screen. An unknown scope id or an empty reason blocks the save.

Available scopes:

ScopeValueSource
user.ip_addressThe runner's IP address as seen by the sandbox's server (request headers). On a local/self-hosted sandbox this is the loopback/bridge address.server
user.pointerThe runner's mouse position — { "x", "y", "viewport": { "width", "height" } } in app-viewport coordinates, snapshotted at run time. Custom views can additionally receive a live stream via ctx.on('pointer.move', cb).client

Declaring scopes extends the entry point with a third argument:

▸python
def process(input: dict, settings: dict, user: dict) -> dict:
    return { "ip": user["ip_address"] }

Every declared scope appears as a key in user (the scope id minus the user. prefix). A key that is present but None means "approved but unavailable" — always handle it. Existing two-argument tools are unaffected; the third argument is passed only when your process accepts it.

Users can revoke their approval at any time from the tool's page; the next run then asks again. Reviewers compare how your code uses user against your declared reason — sending scoped data anywhere the reason doesn't cover fails security review.

integrations

Some tools need to call an API as the person running them — their Jira issues, their Notion pages, their GitHub repos. Never collect tokens through fields for this; declare an integration instead. The user connects the app once, globally, in Settings → Integrations, and grants it to your tool on the same consent flow as scopes. Their credentials live in an encrypted vault and are injected by the platform at request time — they never enter your func's process.

▸json
"integrations": {
  "jira": {
    "scopes": ["read:jira-work"],
    "reason": "Lists your top-priority issues"
  }
}
  • The key is a logical app slug from the platform connector catalog (jira, confluence, github, gitlab, notion, sharepoint, slack, linear, asana, trello, google-drive, and hundreds more — search them in Settings → Integrations). An unknown slug blocks the save.
  • reason is required and user-facing — it is exactly the text shown on the consent screen.
  • scopes lists the provider's OAuth scopes your calls need. The provider's API docs name the required scope next to each endpoint; a wrong declaration simply fails with a provider 403 on your first test run. Users on token/API-key connections aren't affected by scopes (their token acts as their full account).

At runtime, use the SDK's api() client with paths relative to the app's API — the platform pins the base URL per connection (e.g. the user's own Jira site):

▸python
from artifuncs.sandbox_sdk import api, IntegrationError

def process(input, settings):
    resp = api("jira").get("/rest/api/3/search",
                           params={"jql": "assignee=currentUser() ORDER BY priority"})
    if not resp.ok:
        return {"error": f"Jira returned {resp.status}"}
    return {"issues": [i["key"] for i in resp.json()["issues"]]}
▸js
const { api } = require('artifuncs-sdk');

async function process(input) {
  const resp = await api('jira').get('/rest/api/3/search',
    { params: { jql: 'assignee=currentUser() ORDER BY priority' } });
  if (!resp.ok) return { error: `Jira returned ${resp.status}` };
  return { issues: resp.json().issues.map((i) => i.key) };
}

The client exposes get / post / put / patch / delete, each accepting params (query string), headers, and a json or data body. Responses carry status, ok, text, json() and the raw bytes. Upstream responses — including 4xx/5xx — are returned to your code; platform-side failures (app not connected, not granted, not declared) raise IntegrationError (Python) / reject with an IntegrationError (JS) carrying a code.

Notes:

  • api() works only for declared apps, and only after the user approved every declared app — the same all-or-nothing rule as scopes.
  • The user must have the app connected; otherwise calls fail with not_connected and the UI shows a connect prompt.
  • You do not need network for integrations — traffic goes through the platform proxy. Declare network only for anonymous APIs you call directly.
  • Anonymous visitors on /f/<handle> can't connect accounts, so integration tools ask them to sign in instead of running.
  • Reviewers compare your actual calls against the declared scopes and reason — a tool declaring read scopes "to list issues" that also writes data fails security review.

Validation

  • artifuncs.json must be valid JSON. The IDE blocks save on parse errors.
  • name, type, language are required.
  • scopes, when present, must map known scope ids to non-empty reasons.
  • integrations, when present, must map known app slugs from the connector catalog to an object with a non-empty reason (and an optional scopes array of non-empty strings).
  • For INPUT_OUTPUT, fields.input and fields.output must 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).