Inputs and drive
How apps send files to skills — upload, public URL, or base64 inline — and how skills read them.
Jobs accept JSON inputs only — there is no multipart job submission. Files come in via one of three shapes, and the SDK normalizes all of them on the skill side so you write the skill once. This applies to both deterministic (.py) and agentic (.md) skills.
The three input shapes
A frontend (or any caller with a project API key) can pass an image (or any binary) in three ways. Pick by file size and where the file already lives.
// 1. drive_path — file already uploaded to this project's drive
{ "image": { "drive_path": "uploads/abc123.jpg" } }
// 2. url — public HTTPS URL the worker will fetch
{ "image": { "url": "https://example.com/photo.jpg" } }
// 3. data — base64 or full dataURL, inline in the job inputs
{ "image": { "data": "data:image/jpeg;base64,/9j/4AAQSkZJRg..." } }
You can also pass any of these as a bare string ("https://...", "data:...", or a relative drive path like "uploads/abc.jpg"); the SDK detects the shape.
Which to use:
- drive_path for anything > ~200 KB or anything the user uploads from your app. One round-trip uploads it once and keeps it in project storage (so the same file can feed multiple jobs).
- url when the file already lives somewhere fetchable (a CDN, S3 bucket, etc.). The worker downloads it fresh on each job.
- data for tiny inline payloads (icons, signatures, generated SVGs). Job inputs land in Postgres as
jsonb; keep base64 under ~1 MB or you'll bloat the jobs table.
Uploading from an app
const apiKey = "puras_live_AbCdEfGh.SecretSecretSecretSecretSecre32";
// 1) Upload the file
const fd = new FormData();
fd.append("file", file); // a browser File / Blob
// Optional: fd.append("path", "uploads/"); to choose a subfolder
const up = await fetch(`${API_BASE}/v1/drive/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: fd,
}).then(r => r.json());
// → { drive_path: "uploads/<uuid>.jpg", full_path, signed_url, bytes, content_type }
// 2) Submit a job that references the upload
await fetch(`${API_BASE}/v1/jobs`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
skill: "analyze_image",
inputs: { image: { drive_path: up.drive_path } },
}),
});
The same submit body shape works whether analyze_image is a deterministic (.py) or agentic (.md) skill — the worker reads the skill's manifest and dispatches accordingly.
Reading file inputs in a deterministic skill
from puras import load_bytes, load_path
def run(inputs: dict) -> dict:
# Same call handles drive_path, url, data, or a bare string.
img_bytes = load_bytes(inputs["image"])
# Or — get a local filesystem path you can hand to PIL, ffmpeg, etc.
img_path = load_path(inputs["image"], suffix=".jpg")
from PIL import Image
with Image.open(img_path) as im:
return {"width": im.width, "height": im.height, "format": im.format}
The same load_bytes / load_path helpers are available inside per-skill tools declared on an agentic skill — anywhere the worker dispatches a Python callable.
What load_path returns:
drive_pathinputs → the live symlink under./drive/...(lazy read).urlinputs → a temp file downloaded on the spot.data/ base64 inputs → a temp file written from the decoded bytes.
Temp files are cleaned up when the job teardown removes the workdir.
Reading file inputs in a skill (agentic)
Agentic skills have two ways to see file inputs:
-
inputs.attachments— submit a list of files alongside the prompt. The worker resolves each one (drive_path / url / base64) and attaches it to the first user message as a vision or document block the model can look at directly. This is the right path for images and PDFs. -
bashover the drive symlink — the project's drive is mounted at./drive/inside the job. For files the agent only needs to manipulate (resize, transcode, parse) — not visually understand — bash is faster and cheaper than burning vision tokens.
bash: file ./drive/uploads/abc123.jpg
bash: convert ./drive/uploads/abc123.jpg -resize 512x512 ./drive/thumbs/abc123.jpg
When the skill needs to pull a drive file into the model's context mid-run (e.g. after download_url saved a reference image), it calls the platform-provided file_read tool. See agent-attachments for both routes in detail, including the supported MIME types and the model requirement for vision.
Saving outputs back to the drive
Anything a skill (or a per-skill tool) writes under ./drive/... persists in the project's drive bucket and survives the job. Return the relative path so the caller can mint a fresh signed URL with drive_sign(...) (or GET /v1/drive/sign?path=...):
def run(inputs: dict) -> dict:
img_path = load_path(inputs["image"], suffix=".jpg")
from PIL import Image
out_rel = f"thumbs/{img_path.stem}.jpg"
with Image.open(img_path) as im:
im.thumbnail((512, 512))
im.save(f"drive/{out_rel}", "JPEG", quality=85)
return {"thumb": {"drive_path": out_rel}}
The app can then either display the returned signed_url directly or call drive_sign again later.
Drive HTTP API (for apps)
All endpoints accept either a project JWT (dashboard) or a project API key (your app).
| Endpoint | What it does |
|---|---|
POST /v1/drive/upload (multipart file, optional path) | Writes bytes into <project_id>/<path>. Auto-generates uploads/<uuid><ext> when path is empty or a directory. 50 MB cap. Returns {drive_path, full_path, signed_url, bytes, content_type}. |
GET /v1/drive/list?prefix=... | Lists direct children (folders + files) under a project subpath. |
GET /v1/drive/sign?path=...&ttl=... | Mints a short-lived signed URL for a file in the drive. |
DELETE /v1/drive/object?path=... | Deletes a file. If path ends in / or names an extensionless folder, every file underneath is removed recursively. Returns {deleted}. |
GET /v1/drive/zip?prefix=... | Streams every file under prefix as a single zip download. The archive's root folder matches the prefix's last segment. |
GET /v1/drive/origin?path=... | Looks up which job produced a file. Returns {job_id, skill_name, tool, created_at} for media/download outputs, or all-null for files that were uploaded directly. |
POST /v1/drive/share?path=... | Mints a long-lived (10y) signed URL safe to share publicly. Use this for "copy public link" — the URL is effectively permanent and revocable in aggregate by rotating the bucket secret. Returns {url, expires_at}. |
For JWT callers the project_id is a required query/form field; for API-key callers it's inferred from the key.
Conventions
- Always keep inline base64 small. Anything large should go through
/v1/drive/uploadso the bytes don't sit injobs.inputsforever. - Always use
puras.load_bytes/puras.load_pathinstead of branching on input shape inside your skill code — it future-proofs you against new input forms. - Don't trust an arbitrary
urlfrom end-user input without checking who the caller is; the worker will dutifully fetch whatever you point it at (subject to the 50 MB cap and a 60s timeout). - Don't hard-code
<project_id>/prefixes in skill code. Inside a job, drive lives at./drive/; the upload/list/sign endpoints prepend the project prefix for you.
See example-project for a complete project (deterministic skill + agentic skill + app snippet) that exercises this end-to-end. See sdk-media for generating new media.