On this page
On this page
On this page
Before working with any of the methods outlined below, you'll need to initialize the Ouro client.
Make sure you've updated ouro-py to the latest version as we are consistently making updates.
More details can be found in the quickstart.
import os
from ouro import Ouro
ouro = Ouro(api_key=os.environ.get("OURO_API_KEY"))Ouro assets include files, datasets, services, routes, posts, quests, and
conversations. Pass org_id and team_id when creating assets so they land in
the right team (see Teams).
Skip to one of the sections below:
Upload, read, update, and delete files
Structured tabular data with SQL storage
External APIs and route endpoints
Rich text content with the Editor class
Team quests, items, entries, and reviews
Create threads and exchange messages
For every asset on the platform, Ouro stores the following information:
Unless you pass an organization and team, assets are created in your global
organization and default team. Control who can see an asset with visibility.
Choosing the right team keeps activity grouped and helps the right people find the work.
Files are the most basic asset on Ouro. Any file is fair game, and many file types have rich visualizations on the web platform.
Browse or search your files with optional filters for scope, organization, and team.
# List your recent files
files = ouro.files.list()
# Search with a query
files = ouro.files.list(query="climate data", limit=10)
# Scope to an organization
files = ouro.files.list(org_id=org_id, team_id=team_id)You can upload any file, up to 5GB in size.
file = ouro.files.create(
name="cif file",
description="Test file",
visibility="public",
file_path="./Fe.cif",
)
print(file)file_path is the path to the file on your local machine.
If you need to upload multiple files at once, currently the best way is to compress them into a .zip file and upload that.
If you run into issues uploading large files, you can upload them using the web interface. After upload, you'll be able to find the file ID in the details dropdown (cog icon) on the file page header. Once you have the file ID, you can use it to interact with the file programmatically.
id = '48ec6563-5520-4756-ac7f-b38f4933ac95'
file = ouro.files.retrieve(id)
# file is an object with properties like:
# file.id, file.name, file.description, file.metadata, file.visibility, file.created_at, file.userIf you created the file from the SDK, the ouro.files.create response will be a file object which has the ID in the id field.
If you uploaded the file using the web interface, or the asset was created by another user, you can find its ID in the file page header details dropdown (cog icon).
See the highlighted section in the screenshot below:

Once you've retrieved a file object with ouro.files.retrieve, you can read its data using the read_data method.
file_data = file.read_data()
print(file_data.url)The read_data method returns a FileData object which has a url property that you can use to download the file.
import requests
url = file_data.url
response = requests.get(url)
print(response.content)To be able to update a file, you must have admin or write permission on the file. As the creator of a file, you are automatically granted admin permissions.
file_id = file.id
updated_file = ouro.files.update(
id=file_id,
file_path="./Fe2BiNi.cif",
name="Fe2BiNi (Pmmm) updated"
)
# Returns the updated file object
print(updated_file)You can update the file object with any of asset properties using named parameters. Only id is required.
file_path is a special property for files that allows you to update the file data with a new file. This is also optional.
You must have admin permissions on the file to delete it. This will completely remove the file from the platform.
ouro.files.delete(id=file.id)Assets that may have referenced the file will no longer show a connection to the file.
Datasets are structured tabular assets stored in the datasets schema. You can create them from a pandas DataFrame, then retrieve metadata, read the schema, and load/query the data.
Browse or search datasets with optional filters.
datasets = ouro.datasets.list()
datasets = ouro.datasets.list(query="temperature", scope="org", limit=10)Provide a pandas DataFrame and required asset fields. The SDK will infer a SQL schema and upload a preview; if you pass data, rows are inserted into the table.
import pandas as pd
data = pd.DataFrame(
[
{"name": "Bob", "age": 30},
{"name": "Alice", "age": 27},
{"name": "Matt", "age": 26},
{"name": "Bobo", "age": 4},
{"name": "Asta", "age": 15},
]
)
dataset = ouro.datasets.create(
name="preview-dataset",
visibility="public",
monetization="none",
data=data,
)
print(dataset.id)Notes:
table_name (spaces -> underscores, lowercase) stored in dataset.metadata["table_name"].data, the dataset (asset + empty table) is created without rows.Retrieve the dataset object by ID to access standard asset fields plus dataset-specific metadata and preview rows.
dataset_id = "0194f68c-b16e-70d3-8ed3-aafa850272ae"
dataset = ouro.datasets.retrieve(dataset_id)
print(dataset.name, dataset.metadata, dataset.preview[:3])Get column definitions for the underlying table.
columns = ouro.datasets.schema(dataset_id)
for col in columns:
print(col["column_name"], col["data_type"]) # e.g., age integer, name textFetch the dataset's rows as a pandas DataFrame via the Ouro API. Timestamp and date columns are parsed to pandas types.
df = ouro.datasets.query(dataset_id)
print(df.head())Pass a SQL string as the second argument to query() to run a read-only
PostgreSQL query against the dataset's table. Reference the table as
{{table}} - the backend rewrites the placeholder to the fully-qualified
datasets."..." name. Read-only is enforced server-side and queries time
out after 10 seconds. Include LIMIT/OFFSET directly in the SQL.
# Aggregations
totals = ouro.datasets.query(
dataset_id,
"SELECT species, count(*) AS n FROM {{table}} GROUP BY species ORDER BY n DESC",
)
# Time-series buckets
daily = ouro.datasets.query(
dataset_id,
"SELECT date_trunc('day', created_at) AS day, count(*) AS n "
"FROM {{table}} GROUP BY 1 ORDER BY 1",
)
# Sample rows
sample = ouro.datasets.query(dataset_id, "SELECT * FROM {{table}} LIMIT 5")Update asset properties and optionally write rows to the table by passing a DataFrame. Writing new data will replace the existing data in the table.
updated = ouro.datasets.update(
dataset.id,
visibility="private",
)
# Write rows (optional)
data_update = pd.DataFrame([
{"name": "Charlie", "age": 33},
])
updated = ouro.datasets.update(dataset.id, data=data_update)Requires admin permission on the asset.
ouro.datasets.delete(dataset.id)Tips:
public, private, monetized, or organization.External APIs on Ouro are organized with two asset types: services and routes.
A service is a collection of routes that all share the same base URL. Routes are individual endpoints of the API defined by an HTTP method, path, optional URL and query parameters, and an optional body.
Ouro makes no guarantees about the stability of the APIs added to the platform. Make sure you trust the source/creator of the API before sending any sensitive data. You don't need to worry about the security of your account as no Ouro credentials are shared with the API.
If the API endpoint is monetized, you'll only be charged for successful requests.
Not yet supported from the SDK. Use the web interface to create a service.
You can learn more about creating services with our guide How to monetize APIs. While focused on monetizing existing APIs, it covers the basics of creating and adding an API to Ouro.
service_id = "438e454b-cf9e-40d9-b53d-b9b250087179"
service = ouro.services.retrieve(service_id)Like the file object, using ouro.services.retrieve will return a service object with the same base asset properties.
Once you've retrieved a service object, you can interact with routes property.
# Returns a list of Route objects
service_routes = service.read_routes()
# Returns a dictionary with the stored OpenAPI specification as JSON
service_spec = service.read_spec()
print(service_spec['info']['title'])The read_spec() method retrieves the stored OpenAPI specification from our database. This is the parsed specification that was either uploaded or fetched from a remote URL when the service was created. The spec is stored as a JSON object and remains stable even if the original remote spec changes, until the service is updated.
You can execute a service's endpoint with the execute_route method.
route = service_routes[0]
action = service.execute_route(
route.id,
body={"composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000}
)
response = action.final_dataMore details on using route functionality is in the routes section.
Not yet supported from the SDK. Use the web interface to update a service. Navigate the asset and click the edit icon in the asset's header. You'll be able to adjust things like base URL, name, description, and authentication.
To update an individual route, navigate to the route and click the edit button to use the edit form.
Not yet supported from the SDK. Use the web interface to delete a service. Navigate the asset and click the delete button in the settings dropdown in the asset's header.
Routes are the individual endpoints of a service. Each route represents one HTTP endpoint of the underlying web API.
Add a route to an existing service with ouro.routes.create. Pass the parent
service_id and the fields your API expects (method, path, schemas, and so on).
For complex OpenAPI-backed services, creating routes in the web UI
is often easier.
route = ouro.routes.create(
service_id,
name="transcribe",
method="POST",
path="/transcribe",
)To get the details of a route, use the ouro.routes.retrieve method.
You can supply either the route ID or the asset identifier, which consists of
the creator and the route name plus method.
The identifier is the section of the URL after routes/.
Unless you own the route, use the ID because it remains fixed even if the asset
name changes. When an asset name changes, its identifier and URL also change.
route_id = "2443b425-a2bf-4a6f-8202-3ba8b36c921f" # CrystaLLM generate route
route = ouro.routes.retrieve(route_id)
# OR
route_identifier = "mmoderwell/post-generate"
route = ouro.routes.retrieve(route_identifier)
# Returns a Route objectFor file-input routes, inspect the nested route metadata to see compatibility rules:
route = ouro.routes.retrieve(route_id)
print(route.route.input_type) # e.g. "file"
print(route.route.input_filter) # e.g. "audio"
print(route.route.input_file_extensions) # e.g. ["xy", "xye"]input_file_extensions is the preferred field for exact file-format matching.
Older routes may still use the legacy single-value input_file_extension.
For routes that declare multiple named inputs or outputs, see the route input and output assets guide.
Update route metadata with ouro.routes.update. You can pass the route ID or
asset identifier (same as retrieve).
route = ouro.routes.update(
route.id,
description="Transcribe audio files to text",
)Not yet supported from the SDK. Use the web interface to delete a route.
You can execute a route with the execute method. It returns an Action object
with the response, status, input/output assets, logs, and action ID. See
Routes and actions for async runs, webhooks,
and chaining.
route = ouro.routes.retrieve("mmoderwell/post-generate")
action = route.execute(
body={
"composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000
}
)
generation = action.final_dataOther supported parameters to execute include:
query: query string parametersparams: URL path parametersinput_assets: a dictionary mapping route input names to Ouro asset IDsinput_asset: a legacy single asset object to pass to routes with one asset inputYou can chain multiple routes into custom workflows. Many routes create files,
datasets, or posts as outputs. The Python SDK automatically adds saved output
assets to action.final_data.
generation_route = ouro.routes.retrieve("mmoderwell/post-generate")
generation_action = generation_route.execute(
body={
"composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000
}
)
generation = generation_action.final_data
# Read the generated file from the action's final data.
file_id = generation["file"]["id"]
prediction_route = ouro.routes.retrieve("mmoderwell/post-magnetism-curie-temperature")
prediction_action = prediction_route.execute(
input_assets={"file": file_id}
)
prediction = prediction_action.final_data
print(prediction)For routes with multiple named outputs, read outputs by their declared names from
action.final_data, action.output_assets, or the full action record.
Every time a route is executed, Ouro stores the request and response in an action object. Actions track which inputs created which outputs, including any assets created by the route.
actions = route.read_actions()
# Returns a list of Action objects
print(actions)Actions are especially useful for long-running routes. If a route returns
202 Accepted, use the action ID with ouro.routes.retrieve_action,
ouro.routes.poll_action, or action.refresh() to retrieve the result when it
is ready.
Posts on Ouro are a way to share rich text-focused content with the Ouro community. Just like with every other kind of asset, you can create posts with a text editor on the web interface or use the SDK to create them programmatically.
Browse or search posts with optional filters.
posts = ouro.posts.list()
posts = ouro.posts.list(query="machine learning", scope="global", limit=5)The SDK exposes an Editor class that allows you to construct a document block by block.
Blocks are things like paragraphs, headings, lists, or references to other assets.
import pandas as pd
content = ouro.posts.Editor()
content.new_header(level=1, text="Hello World")
content.new_paragraph(text="This is a paragraph")
content.new_code_block(language="python", code="print('Hello, World!')")
content.new_table(pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}))
content.new_inline_asset(id="438e454b-cf9e-40d9-b53d-b9b250087179", asset_type="service", view_mode="card")
content.new_inline_asset(id="8891046c-b52c-432f-b9b0-ca9515bb1c20", asset_type="file", view_mode="preview")Valid methods of the Editor class are:
new_header: adds a header block of specified level (1-3)new_paragraph: adds a paragraph of textnew_code_block: adds a code blocknew_table: adds a table from a pandas DataFramenew_inline_asset: adds an Ouro asset with an optional visualization of the data (view_mode="preview")Using the editor only creates a local object with your content.
You can view your content as JSON and Markdown text using the to_dict method.
Before you can create a post, you need to have the content you want to post.
The Editor class explained above is one way. You can also create an Editor directly from a markdown string:
markdown_string = """
# Hello World
This is a test post from the Python SDK
"""
content = ouro.posts.Editor(text=markdown_string)When the Editor is created via ouro.posts.Editor(), it's automatically connected to the Ouro client. Passing text to the constructor triggers server-side markdown parsing, which handles standard markdown as well as Ouro-specific syntax like @mentions and asset embeds.
You can also call from_markdown explicitly on an existing editor:
content = ouro.posts.Editor()
content.new_header(level=1, text="Hello World")
content.from_markdown("More content parsed from markdown")This is especially useful for converting LLM output. You can ask the model to return Ouro extended markdown with a prompt like this:
Write responses in extended markdown. Mention users as @username, prefer
typed asset links like [results](dataset:<uuid>), and use
assetComponent blocks when the reader needs a rich preview.
```assetComponent
{
"id": "<uuid>",
"assetType": "dataset",
"viewMode": "preview",
"displayConfig": {
"visualizationId": "<dataset-view-uuid-or-null>",
"actionId": "<route-action-uuid-or-null>"
}
}
```When you embed an asset, you don't need to provide the title, description, or
link; Ouro renders those from the asset. Set assetType to post, file,
dataset, route, or service. For files and datasets, prefer preview;
otherwise use card. For datasets, set
displayConfig.visualizationId to render a saved dataset view. For routes, set
displayConfig.actionId to pin a specific run.
Once you have your content, you save it to Ouro with the ouro.posts.create method.
post = ouro.posts.create(
content=content,
name="Hello World",
description="This is a post from the Python SDK",
visibility="public",
)
# Returns a Post object
print(post)If you don't need block-by-block control, you can skip the Editor entirely and pass markdown or a file path directly:
post = ouro.posts.create(
name="Hello World",
content_markdown="# Hello World\nThis is a test post from the Python SDK",
visibility="public",
)
# Or from a markdown file
post = ouro.posts.create(
name="Hello World",
content_path="./my-post.md",
visibility="public",
)Provide exactly one of content, content_markdown, or content_path.
Note: If you start your content with an H1, the name of the post should match the H1.
You can read a post with the ouro.posts.retrieve method.
You can find the ID of a post in the response of the ouro.posts.create method or from the web interface.
post_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"
post = ouro.posts.retrieve(post_id)
# Returns a Post object
print(post)You will find the post's content in the content property of the post object.
You can get a markdown representation of the content with the text property.
post_markdown = post.content.text
print(post_markdown)You can update a post with the ouro.posts.update method.
updated_post = ouro.posts.update(
post.id,
description="This is a post from the Python SDK that is now private",
visibility="private"
)
# Returns the updated post object
print(updated_post)You can update any of the post's properties, including the content, using the same approach used to create it.
You can delete a post with the ouro.posts.delete method. You must be an admin of the post to delete it.
ouro.posts.delete(post.id)Quests are team-scoped requests for help. Use ouro.quests to create quests,
manage items, submit entries, and review contributions. See
Quests on Ouro for the product model.
quest = ouro.quests.create(
name="CeO2 reference patterns",
description="Collect reference XRD patterns for CeO2 nanoparticles.",
visibility="organization",
org_id=org_id,
team_id=team_id,
type="closable",
status="open",
items=[
"Upload a clean .xy or .xye pattern",
{"description": "Write a short methods note", "expected_asset_type": "post"},
],
)quest = ouro.quests.retrieve(quest_id)
ouro.quests.update(quest_id, status="closed")items = ouro.quests.list_items(quest_id)
ouro.quests.create_items(quest_id, ["Additional benchmark file"])
entry = ouro.quests.create_entry(
quest_id,
item_id=items[0].id,
asset_id=dataset.id,
asset_type="dataset",
)
entries = ouro.quests.list_entries(quest_id, status="submitted")
ouro.quests.review_entry(quest_id, entry.id, status="accepted")type)Set type when creating a quest ("closable" default, or "continuous"):
type | create_entry behavior |
|---|---|
closable | At most one active entry per (item_id, your user) while status is submitted or accepted. A second call raises an API error. After rejection, you may submit again. |
continuous | No per-user cap — each create_entry inserts a new row for the same item. |
# Ongoing benchmark — contributors can submit every week
ouro.quests.create(
name="Weekly structure upload",
type="continuous",
items=["Upload this week's relaxed structure"],
org_id=org_id,
team_id=team_id,
)
# One-shot bounty — one pending/accepted slot per contributor per item
ouro.quests.create(
name="Reference XRD pattern",
type="closable",
items=["Upload a clean .xy pattern"],
org_id=org_id,
team_id=team_id,
)Inspect quest.quest.type (nested on the retrieved asset) before retrying failed
submits. Use assets={key: asset_id} for multi-input items; see
Quests on Ouro.
ouro.quests.delete(quest_id)Conversations let users exchange messages. You can create, list, retrieve, update, and delete conversations, and create or list messages within a thread.
Start a conversation by passing member user IDs. Include yourself if you want the thread to appear in your conversation list.
conversation = ouro.conversations.create(
member_user_ids=[my_user_id, teammate_user_id],
name="Project Alpha",
org_id=org_id,
team_id=team_id,
)Start by listing your conversations to find threads you want to work with.
conversations = ouro.conversations.list()
for c in conversations:
print(c.id, c.name, c.metadata)Once you have an ID, load the conversation to inspect metadata and access message helpers.
conversation_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"
conversation = ouro.conversations.retrieve(conversation_id)
print(conversation.name, conversation.metadata)You can update top-level fields like name and summary.
updated = ouro.conversations.update(
conversation_id,
name="Project Alpha",
summary="Research thread"
)
print(updated)Send a message to a conversation as plain text or structured JSON. Use the
conversation.messages.create helper.
# Text message
msg = conversation.messages.create(text="Hello team!")
# JSON message (rich content)
editor = ouro.posts.Editor()
editor.new_paragraph(text="Hello team!")
msg2 = conversation.messages.create(json=editor.to_dict())Read the latest messages to understand the current context of the thread.
messages = conversation.messages.list()
for m in messages:
print(m.get("id"), m.get("text") or m.get("json"))ouro.conversations.delete removes the conversation when you are the only
member. Otherwise it removes you from the member list (leave the thread).
ouro.conversations.delete(conversation_id)Comments are lightweight, rich-text notes attached to any asset (files, datasets, posts, routes, services, conversations). One-level threads are supported: top-level comments on an asset, and replies to those comments. Use the built-in Editor to compose content, then create, list, and update comments.
Compose with the Editor, then create a comment on a parent asset by ID.
parent_asset_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91" # can be any asset id
editor = ouro.comments.Editor()
editor.new_paragraph(text="Great post! I especially liked the dataset example.")
comment = ouro.comments.create(
parent_id=parent_asset_id,
content=editor,
visibility="public",
)
print(comment.id)Fetch all top-level comments attached to an asset.
comments = ouro.comments.list_by_parent(parent_asset_id)
for c in comments:
# The content is stored as text + JSON structure
print(c.id, c.content.text)fetched = ouro.comments.retrieve(comment.id)
print(fetched.content.text)Rebuild the content with Editor or pass an updated Content object.
update_editor = ouro.comments.Editor()
update_editor.new_paragraph(text="Edited: adding one more note.")
updated_comment = ouro.comments.update(
comment.id,
content=update_editor,
visibility="private",
)
print(updated_comment)Replies are simply comments whose parent is a comment. Only one level of replies is supported.
# Create a reply to a top-level comment
reply_editor = ouro.comments.Editor()
reply_editor.new_paragraph(text="Replying here with more details.")
reply = ouro.comments.create(
parent_id=comment.id, # parent is the comment id
content=reply_editor,
visibility="public",
)
# List replies for a top-level comment
replies = ouro.comments.list_replies(comment.id)
for r in replies:
print(r.id, r.content.text)