Playground schema conventions
How input_schema and output_schema drive auto-generated UI forms (contentMediaType + x-puras extensions).
A "playground" is any UI that renders a form for a skill's inputs from its
declared input_schema and shows the resulting output_schema. The dashboard
ships one; you can build your own with the same key.
A bare type: string carries no UX hint. To tell a playground "this is an
image upload" or "this should be a dropdown," skill authors add two layers of
metadata that JSON Schema validators silently ignore:
contentMediaType— standard JSON Schema (Draft 2020-12). Tells the playground what kind of content the string carries.x-puras— a Puras-specific extension block with richer widget hints (x-*keys are reserved for non-validating extensions in JSON Schema).
Skills don't need any of this to run — the worker only enforces the type constraints. The metadata is purely for the rendering layer.
The x-puras block
x-puras:
widget: image # see widget vocabulary below
accept: ["image/jpeg", "image/png", "image/webp"] # widget-specific
max_size_mb: 5
upload: drive # how the playground should hand the file back
placeholder: "drag photo here"
help: "tam boy, net bir fotoğraf önerilir"
All fields are optional. Unknown fields are ignored by the playground (so you can prototype your own).
Widget vocabulary
widget | What the playground renders | Schema shape it pairs with |
|---|---|---|
image | Drop-zone + URL field + preview | string (URL) or the polymorphic file object |
video | Same as image, video preview | string (URL) or file object |
audio | File picker, audio preview | string (URL) or file object |
file | Generic file picker | string (URL) or file object |
drive_file | Picker over the project's drive | string (drive_path) |
attachments | Multi-file drop-zone | array of file objects |
text | Single-line <input> | string |
long_text | <textarea> | string, usually maxLength >= 500 |
code | Monospace editor (lang from x-puras.lang) | string |
select | Dropdown | string with enum |
multiselect | Multi-select | array of string with items.enum |
switch | Toggle | boolean |
slider | Range slider | number with minimum + maximum |
Default widget if unset:
boolean→switchstringwithenum→selectstringwithcontentMediaTypematchingimage/*→imagestringwithcontentMediaTypematchingvideo/*→videostringwithcontentMediaTypematchingaudio/*→audiostringwithmaxLength >= 500→long_textarrayof strings withitems.enum→multiselectarrayof file objects →attachments- everything else
string→text
File inputs — two shapes
A skill that needs a binary input can declare it two ways:
A. As a URL string (simplest)
user_image_url:
type: string
format: uri
contentMediaType: image/*
x-puras:
widget: image
upload: drive
accept: ["image/jpeg", "image/png", "image/webp"]
max_size_mb: 5
description: User's photo URL (HTTPS).
The playground uploads to POST /v1/drive/upload, takes the response's
signed_url, and passes that string into inputs.user_image_url. The skill
just sees a URL.
B. As a polymorphic file object (puras-native)
user_image:
type: object
x-puras:
widget: image
accept: ["image/jpeg", "image/png", "image/webp"]
oneOf:
- { required: [drive_path], properties: { drive_path: {type: string} } }
- { required: [url], properties: { url: {type: string, format: uri} } }
- { required: [base64], properties: { base64: {type: string}, media_type: {type: string} } }
The skill reads it with puras.load_bytes(inputs["user_image"]) or
puras.load_path(...) — these helpers accept all three shapes transparently.
This form is more flexible (frontend can send a drive_path, an external URL,
or inline base64) at the cost of more verbose validation.
Pick A when the skill itself only needs a URL (e.g. handing off to another model). Pick B when the skill code wants raw bytes or a local path.
upload modes
For widgets that produce a file (image, video, audio, file):
drive(default) — playground POSTs to/v1/drive/uploadand sends the signed URL back into the input. Persists across jobs; recommended for anything > 200 KB.inline— playground base64-encodes and sends inline. Use for tiny payloads only (job inputs live forever in Postgresjsonb).url-only— no upload widget; user pastes a URL. For skills that should only consume public URLs.
Long text and code
prompt:
type: string
minLength: 1
maxLength: 4000
x-puras:
widget: long_text
placeholder: "Tell the agent what to do"
sql:
type: string
x-puras:
widget: code
lang: sql
Enums
enum alone gives you a select. Add labels with x-puras.options:
tone:
type: string
enum: [playful, bold, trustworthy]
x-puras:
widget: select
options:
- { value: playful, label: "🎉 Playful" }
- { value: bold, label: "🔥 Bold" }
- { value: trustworthy, label: "🛡 Trustworthy" }
Arrays
For an array of attachments (e.g. agentic skill that accepts vision inputs):
attachments:
type: array
minItems: 1
maxItems: 4
x-puras:
widget: attachments
accept: ["image/jpeg", "image/png", "image/webp", "application/pdf"]
max_size_mb: 5
items:
type: object
properties:
drive_path: { type: string }
url: { type: string, format: uri }
base64: { type: string }
media_type: { type: string }
additionalProperties: true
Playground renders one drop-zone that accepts multiple files and submits them as the array.
Output rendering
output_schema doesn't get widget hints — playgrounds render it as
read-only structured data. But the same contentMediaType convention
applies: an output field with contentMediaType: image/* should be
rendered as an <img>, audio as <audio>, etc. A playground can also
honor x-puras.widget on outputs if you want a specific renderer (e.g.
widget: code to syntax-highlight a returned string).
What playgrounds MUST NOT do
- Hide a field because it has no
x-purasblock — fall back to defaults. - Reject unknown
widgetvalues — render a placeholder warning, keep the form usable. - Strip
x-purasfrom the request body. Sendinputsexactly as the schema describes; the worker only cares about validated values.
See concepts for how skills, inputs, and outputs are stored. See the shipped example-project for a working skill that uses these hints.