---
title: "Route input and output assets"
description: "How to declare, execute, debug, and build Ouro routes that consume and create assets."
date: "2026-05-05"
last_updated: "2026-05-05"
---

Routes 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.

> **Canonical shape.** The plural, keyed `input_assets` / `output_assets`
> declarations are the canonical model. Ouro still writes a synchronized
> single-asset projection (`input_type`, `output_type`, `input_file_extension`,
> `output_file_extension`) on every route so older clients and indexed
> lookups keep working, but new code should read and write the plural
> shape.

## 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_id` and `input_asset` point at the primary input asset.
- `output_asset_id` and `output_asset` point at the primary output asset.

For new work, prefer the keyed arrays:

- `action.input_assets`
- `action.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.

```json
{
  "structure": {
    "asset_type": "file",
    "file_extensions": ["cif"],
    "primary": true
  },
  "training_data": {
    "asset_type": "dataset"
  }
}
```

The key is the name callers use in `input_assets`. Ouro places the resolved asset value at the same key in the forwarded JSON body, so choose keys that match your service request schema.

For file inputs, use `file_extensions` for exact formats like `["cif", "xyz"]`. Use `input_filter` only for broader categories like `image`, `audio`, or `video`.

Mark the declaration that should drive the legacy `input_type` projection (and which input is billed for variable-cost routes) with `"primary": true`. When omitted, Ouro promotes the first declaration to primary.

### Output Assets

`route.route.output_assets` is a record keyed by response field name.

```json
{
  "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`. Routes without keyed `output_assets` do not create output assets automatically.

## 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.

```python showLineNumbers
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 present
```

Execute with normal JSON in `body`, URL path values in `params`, query string values in `query`, and Ouro asset references in `input_assets`.

```python showLineNumbers
action = route.execute(
    body={
        "model": "orb-v3-conservative-inf-mpa",
        "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 a service needs an asset at a specific request key, the route should declare that key directly in `x-ouro-input-assets`.

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:

```python showLineNumbers
{
    "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:

```python showLineNumbers
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."

```python showLineNumbers
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`.

```python showLineNumbers
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",
            "file_extensions": ["cif"],
            "primary": True,
        }
    },
)
@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`.

```python showLineNumbers
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.

```python showLineNumbers
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.

```python showLineNumbers
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.

```python showLineNumbers
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.

```python showLineNumbers
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](/guides/long-running-apis) 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_assets` and `route.route.output_assets`.
- Confirm the `input_assets` keys passed by the caller exactly match the route declaration keys.
- Confirm the service request schema uses the same input keys declared in `input_assets`.
- Confirm service response keys exactly match `output_assets` keys.
- Check `action.response` for the raw response and `action.output_assets` for assets Ouro saved.
- For async routes, inspect the final action after the webhook completes rather than the first `202 Accepted` response.

```python showLineNumbers
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.
