Route input and output assets
How to declare, execute, debug, and build Ouro routes that consume and create assets.
Last updated May 5, 2026
8 minute readRoutes can do more than accept plain JSON. They can take existing Ouro assets as inputs, resolve those assets into the request body sent to your service, and save selected response fields back to Ouro as files, datasets, or posts.
The moving parts are:
- Route declarations describe which request body keys accept assets and which response keys should become assets.
- Actions record each route execution.
- Action assets connect an action to every named input and output asset used during that execution.
This guide explains how to read those pieces, how to call routes with ouro-py, and how to build services that participate in the flow.
Mental Model
When someone executes a route, Ouro creates an action and forwards a request to the service.
If the caller passes input_assets, Ouro resolves each asset ID before forwarding the request. A file becomes file metadata plus a signed URL, a dataset becomes rows and metadata, and a post becomes content and metadata. The resolved value is inserted into the service request body at the declared key.
When the service responds, Ouro stores the raw response on the action. If the route declares output assets, Ouro also looks for matching keys in the response, creates or records those assets, and links them back to the action.
The older single-asset fields still exist:
input_asset_idandinput_assetpoint at the primary input asset.output_asset_idandoutput_assetpoint at the primary output asset.
For new work, prefer the keyed arrays:
action.input_assetsaction.output_assets
Each row has name, asset_id, asset_type, is_primary, and usually a hydrated asset object.
Route Declarations
Use route metadata to tell Ouro which assets a route accepts and creates.
Input Assets
route.route.input_assets is a record keyed by request body name.
{
"structure": {
"asset_type": "file",
"input_file_extensions": ["cif"],
"body_path": "structure"
},
"training_data": {
"asset_type": "dataset",
"body_path": "inputs.training_data"
}
}The key is the name callers use in input_assets. By default, Ouro places the resolved asset value at the same key in the forwarded JSON body. Use body_path when the service expects the asset at a nested path.
For file inputs, use input_file_extensions for exact formats like ["cif", "xyz"]. Use input_filter only for broader categories like image, audio, or video.
Output Assets
route.route.output_assets is a record keyed by response field name.
{
"report": {
"asset_type": "post",
"primary": true
},
"relaxed_structure": {
"asset_type": "file",
"file_extensions": ["cif"]
},
"summary": {
"asset_type": "file",
"file_extensions": ["json"]
}
}When the service response contains one of those keys, Ouro saves that value as the declared asset type. primary: true controls the legacy output_asset_id and makes that output appear first in action.output_assets.
If a route has no keyed output_assets, Ouro still supports the legacy shape where the response has one top-level file, dataset, or post key.
Calling Routes With ouro-py
Start by inspecting the route. list() results are intentionally light, so retrieve the route before relying on nested route metadata.
import os
from ouro import Ouro
ouro = Ouro(api_key=os.environ["OURO_API_KEY"])
route = ouro.routes.retrieve("matsci/relax-structure")
print(route.route.input_assets)
print(route.route.output_assets)
print(route.route.input_type) # Legacy single input type, if present
print(route.route.input_file_extensions) # File compatibility, if presentExecute with normal JSON in body, URL path values in params, query string values in query, and Ouro asset references in input_assets.
action = route.execute(
body={
"model": "orb-v3",
"fmax": 0.05,
},
input_assets={
"structure": "00000000-0000-0000-0000-000000000001",
},
output={
"visibility": "private",
"team_id": "00000000-0000-0000-0000-000000000002",
},
)
print(action.status)
print(action.input_assets)
print(action.output_assets)
result = action.final_data
relaxed_structure = result["relaxed_structure"]Most callers should pass bare asset IDs. If the service needs the resolved asset at a nested request path, the route author should declare body_path in x-ouro-input-assets; callers should not need to know the forwarded service body shape.
The SDK also accepts object values for older or hand-authored routes that did not declare enough metadata, but treat that as an escape hatch for migration and debugging rather than a normal integration pattern.
action.final_data merges any saved output assets back into the response under their output names. If a route returns {"energy": -8.1} and saves a relaxed_structure file, final_data will include both:
{
"energy": -8.1,
"relaxed_structure": {
"id": "00000000-0000-0000-0000-000000000003",
"asset_type": "file",
"name": "Relaxed Fe2O3"
}
}For long-running routes, return an action handle immediately and poll it later:
action = route.execute(
input_assets={"structure": structure_id},
wait=False,
)
print(action.id, action.status)
finished = ouro.routes.poll_action(
str(action.id),
poll_interval=10,
timeout=3600,
)
print(finished.final_data)Chaining Routes
Because output assets are linked to actions, route chaining is usually just "run a route, take a named output asset, pass it to the next route."
relax_route = ouro.routes.retrieve("matsci/relax-structure")
phonon_route = ouro.routes.retrieve("matsci/phonon-band-structure")
relax_action = relax_route.execute(
input_assets={"structure": starting_cif_id},
)
relaxed_file_id = relax_action.final_data["relaxed_structure"]["id"]
phonon_action = phonon_route.execute(
input_assets={"structure": relaxed_file_id},
wait=True,
poll_timeout=3600,
)
print(phonon_action.final_data["report"]["id"])If a route declares multiple outputs, prefer the output name over guessing by asset type. Names like report, summary, relaxed_structure, and candidate_cifs are stable workflow handles.
Building Services
Service builders declare asset behavior in OpenAPI. With FastAPI, use the helpers from ouro-py.
from fastapi import FastAPI, Header
from fastapi.openapi.utils import get_openapi
from ouro.utils import get_custom_openapi, ouro_field
from typing import Optional
app = FastAPI(title="Materials Service", version="1.0.0")
@ouro_field(
"x-ouro-input-assets",
{
"structure": {
"asset_type": "file",
"input_file_extensions": ["cif"],
"body_path": "structure",
}
},
)
@ouro_field(
"x-ouro-output-assets",
{
"relaxed_structure": {
"asset_type": "file",
"file_extensions": ["cif"],
"primary": True,
},
"summary": {
"asset_type": "post",
},
},
)
@app.post("/relax")
async def relax_structure(
request: dict,
ouro_action_id: Optional[str] = Header(None, alias="ouro-action-id"),
ouro_route_org_id: Optional[str] = Header(None, alias="ouro-route-org-id"),
ouro_route_team_id: Optional[str] = Header(None, alias="ouro-route-team-id"),
):
structure = request["structure"]
relaxed_cif_bytes = run_relaxation(structure["url"])
energy = -8.1
return {
"energy": energy,
"relaxed_structure": {
"name": f"Relaxed {structure['name']}",
"description": "Relaxed crystal structure produced by the route.",
"type": "chemical/x-cif",
"extension": "cif",
"base64": encode_base64(relaxed_cif_bytes),
"org_id": ouro_route_org_id,
"team_id": ouro_route_team_id,
},
"summary": {
"name": f"Relaxation report for {structure['name']}",
"content": {
"text": f"Final energy: {energy} eV"
},
},
}
app.openapi = get_custom_openapi(app, get_openapi)The response keys must match x-ouro-output-assets. In this example, Ouro saves relaxed_structure as a file and summary as a post.
Output Payload Shapes
For a file, return a payload with either base64 or url, plus extension, type, and a useful name.
return {
"plot": {
"name": "Phase diagram",
"description": "Interactive phase diagram HTML.",
"type": "text/html",
"extension": "html",
"base64": encode_base64(html_bytes),
}
}For a dataset, return row data.
return {
"predictions": {
"name": "Candidate scores",
"description": "Ranked candidate materials.",
"data": [
{"formula": "LiCoO2", "score": 0.91},
{"formula": "NaCoO2", "score": 0.87},
],
}
}For a post, return content with markdown text or editor JSON.
return {
"report": {
"name": "Screening report",
"description": "Summary of the route run.",
"content": {
"text": "# Screening report\n\nThe best candidate was LiCoO2."
},
}
}If your service already created an Ouro asset itself, return the existing asset object or an object with its id. Ouro will record the action output without creating a duplicate.
return {
"report": {
"id": created_post.id,
"asset_type": "post",
}
}Async Services
For long-running services, return 202 Accepted quickly and send the final response to the webhook URL Ouro provides in request headers. The webhook response uses the same output asset shape as a synchronous response.
import requests
from fastapi import Header, status
from fastapi.responses import JSONResponse
from typing import Optional
@ouro_field("x-ouro-execution-mode", "async")
@ouro_field("x-ouro-output-assets", {"report": {"asset_type": "post", "primary": True}})
@app.post("/explore")
async def explore(
request: dict,
ouro_webhook_url: Optional[str] = Header(None, alias="ouro-webhook-url"),
ouro_webhook_token: Optional[str] = Header(None, alias="ouro-webhook-token"),
):
run_exploration_in_background(
request=request,
webhook_url=ouro_webhook_url,
webhook_token=ouro_webhook_token,
)
return JSONResponse(
status_code=status.HTTP_202_ACCEPTED,
content={"status": "accepted"},
)
def send_result(webhook_url: str, webhook_token: str, report_markdown: str):
requests.post(
webhook_url,
headers={"ouro-webhook-token": webhook_token},
json={
"status": "completed",
"response": {
"report": {
"name": "Exploration report",
"content": {"text": report_markdown},
}
},
},
timeout=30,
)See the long-running APIs guide for the full webhook pattern.
Debugging Checklist
When a route does not show the inputs or outputs you expected:
- Retrieve the route and inspect
route.route.input_assetsandroute.route.output_assets. - Confirm the
input_assetskeys passed by the caller exactly match the route declaration keys. - Confirm
body_pathmatches the JSON shape your service expects. - Confirm service response keys exactly match
output_assetskeys. - Check
action.responsefor the raw response andaction.output_assetsfor assets Ouro saved. - For async routes, inspect the final action after the webhook completes rather than the first
202 Acceptedresponse.
action = ouro.routes.retrieve_action(action_id)
print(action.status)
print(action.response)
print(action.input_assets)
print(action.output_assets)
logs = ouro.routes.get_action_logs(action_id, chronological=True)
for log in logs:
print(log.level, log.message)The fastest way to stay oriented is to treat the route declaration as the contract, the service response as the source material, and the action as the audit trail that proves what was consumed and what was created.
On this page