| name | discover-api |
| description | Query the Ionworks API /discovery endpoints before any API operation. Use when the user asks to interact with the Ionworks API, battery data, cell specifications, measurements, simulations, pipelines, or UCP protocols. Triggers: "ionworks", "battery data", "cell spec", "measurement", "simulation", "pipeline", "UCP", "cycler", "SDK", "Python client". |
Ionworks API Discovery
Before making any request to the Ionworks API, fetch
GET /discovery/capabilities to load domain context,
auth requirements, and schema references. This avoids
guessing at endpoints or data shapes.
When to Use
Use this skill when the user:
- Asks to read, create, or modify battery data
- Wants to run a simulation or work with UCP protocols
- Mentions cell specifications, instances, or measurements
- Needs to explore what the Ionworks API offers
- Asks about data hierarchy or schema
- Wants to run a pipeline or parameterization
Do NOT use this skill for:
- Frontend-only changes with no API calls
- Infrastructure or deployment tasks unrelated to the API
Step 1: Fetch Capabilities, Then the Relevant Schema (MANDATORY)
You MUST do this before any other API call. Do NOT
skip this step. Do NOT guess at data shapes, column
names, metadata fields, or entity structures — they are
all defined in these responses.
from ionworks import Ionworks
client = Ionworks()
caps = client.capabilities()
caps returns the data hierarchy, what each entity
contains (domain_context.key_concepts), auth
requirements, and which schemas are available
(schemas key). Then fetch whichever schema is
relevant to the user's query:
schema = client.schema("data")
schema = client.schema("protocol")
schema = client.schema("project")
schema = client.schema("study")
schema = client.schema("model")
schema = client.schema("parameterized_model")
schema = client.schema("optimization")
schema = client.schema("pipeline")
Each per-resource schema includes create_schema,
update_schema, and response_schema (where
applicable), plus extras like mapping_endpoints (for
study) or element_types (for pipeline). Read caps
and the relevant schema response before writing any
query or analysis code.
Use entity ids verbatim — never retype them
When an id is handed to you (page context, a prior tool
result, the user's message), copy it exactly — assign
it to a variable in one go rather than retyping it
digit-by-digit into code, where a single transposed
character yields a confusing 404. If a lookup 404s on an
id you believe is correct, don't keep guessing: list
the resource and match by name to recover the real id.
Protocol schema — YAML authoring guide
client.schema("protocol") returns
input_format_description, step_types,
end_condition_forms, runnable examples, and
input_schema (a JSON Schema, generated from the
parser's own enums and parity-tested against
validate_protocol_from_dict). Validate authored
protocols locally with jsonschema.validate(...)
against input_schema before round-tripping through
/protocols/validate. UCP protocols are authored as
YAML where each step is a single-key dict keyed by
step type:
steps:
- Charge:
mode: C-rate
value: "1"
ends:
- Voltage > 4.2
- step_block:
name: cycling
repeat: 100
steps:
- Discharge:
mode: C-rate
value: "1"
ends:
- Voltage < 2.5
Copy from examples for the canonical shape.
Step 2: Use the Python Client
Prefer the ionworks Python client over raw HTTP. It
handles auth, retries, signed-URL uploads, pagination,
and parallel fetching automatically. Tabular data
(time_series, steps, cycles) is returned as Polars
DataFrames by default.
from ionworks import Ionworks
client = Ionworks()
Sub-clients
| Sub-client | Methods |
|---|
client.project | .list(), .get(project_id), .create(data), .update(project_id, data), .delete(project_id) |
client.model | .list(), .get(model_id), .create(data), .update(model_id, data), .delete(model_id), .add_custom_variable(model_id, data) |
client.parameterized_model | .list_by_cell_specification(cell_spec_id), .list_by_project(project_id=, cell_spec_id=), .get(id), .create(cell_spec_id, data), .create_or_get(cell_spec_id, data), .update(cell_spec_id, pm_id, data), .get_parameter_values(id), .get_variable_names(id) |
client.study | .list(project_id), .get(project_id, study_id), .create(project_id, data), .update(...), .delete(...), .assign_simulation(...), .remove_simulation(...), .assign_measurement(...), .list_measurements(...), .remove_measurement(...) |
client.cell_spec | .list(), .get(id), .create(data), .create_or_get(data), .update(id, data), .delete(id) |
client.cell_instance | .list(cell_spec_id), .get(id), .create(cell_spec_id, data), .create_or_get(cell_spec_id, data), .update(id, data), .delete(id), .detail(id) |
client.cell_measurement | .list(instance_id), .get(id), .detail(id), .steps(id), .cycles(id), .steps_and_cycles(id), .time_series(id), .create(instance_id, data), .create_or_get(instance_id, data), .create_properties(instance_id, name, properties), .create_file(instance_id, name, filepaths), .update(id, data), .delete(id), .list_files(id), .download_files(id), .get_file(id, filename) |
client.material | .list(), .get(material_id) (read-only; no name filter — filter client-side). list() accepts an optional project_id= that only scopes each material's property_count to that project — it does not filter which materials return (materials are org-wide). Cell components are not a sub-client: list via client.get("/cell_components"). See manage-cells for the material → component → spec reverse lookup. |
client.material_property_dataset | .list(material_id=), .get(dataset_id), .get_units(dataset_id) → dict[str, str], .get_data(dataset_id) → DataFrame. See electrolyte-transport for a worked example. |
client.electrolyte | .transport_from_dataset(dataset_id, forms=, columns=, concentration_column=) → pybamm.ParameterValues of concentration-dependent electrolyte transport (tabulated interpolant or fitted Landesfeind form per property), ready for a DirectEntry. See electrolyte-transport. |
client.simulation | .protocol(config), .protocol_batch(config), .list(parameterized_model_id=, study_id=), .get(id), .get_result(id), .wait_for_completion(id) |
client.pipeline | .create(config), .list(project_id=, limit=), .get(id), .result(id), .wait_for_completion(id), .get_element_metadata(pipeline_id, element_name) |
client.simple_pipeline | .create(config, project_id=, name=, description=), .list(project_id=, limit=, offset=, name=, status=, order_by=, order=), .get(id), .update(id, name=, description=), .cancel(id), .delete(id), .wait_for_completion(id, timeout=, verbose=) |
client.optimization | .run(data), .get(id), .list(project_id=), .update(id, data), .cancel(id), .wait_for_completion(id) |
client.protocol | .validate(protocol_yaml), .find_input_references(protocol_yaml) |
client.ecm | .list_examples(), .get_example_data(id), .fit_from_example(id, num_rcs=, fit_ocv=, ...) (sync), .fit_from_file(file, num_rcs=, ...) (returns EcmFitJob), .fit_from_measurements(config) (returns EcmFitJob), .wait_for_completion(job, timeout=, ...) → FitResults, .detect_and_read(file), .save_to_project(name, cell_spec_id, fit_results, ...) |
client.job | .create(payload), .get(id), .get_metadata(id), .get_parameter_trace(id), .list(), .cancel(id) |
client.organization | .usage() → OrganizationUsage — usage + limits for the org that owns the API key, for the current calendar-month period (resets on the 1st; see .period_start / .period_end). .simulation has .usage and .limit; .compute also has .usage_by_type (per-job-type breakdown: simulation/datafit/optimization/validation/pipeline). All usage/limit values are in hours; a None .limit means that type is unconstrained. |
client.urls | .project(project_id=), .model(model_id, project_id=), .parameterized_model(pm_id, project_id=), .optimization(optimization_id, project_id=), .pipeline(pipeline_id, project_id=), .study(study_id, project_id=), .simulation(simulation_id, parameterized_model_id=, project_id=), .protocol(protocol_id, project_id=), .material(material_id, project_id=), .cell_specs(project_id=), .cell_instances(spec_id, project_id=), .measurement(measurement_id, project_id=) — build web-app links for a resource. The host is derived from the client's api_url (api.ionworks.com → app.ionworks.com); override with IONWORKS_APP_URL. project_id defaults to the client's (IONWORKS_PROJECT_ID). |
client (direct) | .capabilities(), .schema(name) where name is "data", "protocol", "project", "study", "model", "parameterized_model", "optimization", or "pipeline", .health_check(), .whoami() |
Direct HTTP fallback
When the client doesn't cover an endpoint:
data = client.get("/some/endpoint")
data = client.post("/some/endpoint", {"key": "value"})
client.patch("/some/endpoint", {"key": "value"})
client.delete("/some/endpoint")
Step 3: Use the Responses — Do Not Guess
Use column names, field names, and entity structures
from the responses you already fetched — do not
hardcode or guess them.
Step 4: Navigate the Data Hierarchy
organization
-> project
-> cell_specification
-> cell_instance
-> cell_measurement
-> [time_series, steps, cycles]
-> parameterized_model
-> study
-> simulation_mappings
-> measurement_mappings
Cached navigation (Navigator)
For scripts that walk the spec → instance → measurement
hierarchy and need to fetch each entity at most once,
use :class:ionworks.Navigator. It memoises every list,
paginates automatically, and returns listings sorted by
name.
from ionworks import Ionworks, Navigator
nav = Navigator(Ionworks())
for name, spec in nav.specs().items():
for inst in nav.instances(name):
for m in nav.measurements(inst.id):
ts = nav.time_series(m.id)
Pagination
All list() methods on cell_spec, cell_instance,
cell_measurement, material, and material_property_dataset return a
PaginatedList with .items, .count, and .total, and accept
limit=/offset=.
PaginatedList behaves like a regular list (iteration,
indexing, len, truthiness), so existing code that treats the
return value as a list keeps working.
Iterating a result only visits one page. A bare
list() returns a single page (server default), and looping
over it visits just .items — it does not auto-fetch
later pages. For an exhaustive scan, page until you've
collected .total rows:
specs = client.cell_spec.list()
for spec in specs:
print(spec.name)
page = client.cell_spec.list(limit=10, offset=0)
page.total
page.count
def fetch_all(list_fn, page_size=500, **kwargs):
items, offset = [], 0
while True:
page = list_fn(limit=page_size, offset=offset, **kwargs)
items.extend(page)
offset += len(page)
if offset >= page.total or not len(page):
return items
all_specs = fetch_all(client.cell_spec.list)
Filtering and Ordering
Cell specs, instances, and measurements support
keyword-only filter parameters:
specs = client.cell_spec.list(
name="LGM50",
name_exact="LGM50 Demo",
form_factor="cylindrical",
created_by_email="user@",
created_after="2025-01-01",
order_by="name",
order="asc",
)
Common filter parameters across cell list methods:
name — case-insensitive substring match
name_exact — exact match
created_by_email — substring match on creator email
created_after / created_before — date range
updated_after / updated_before — date range
order_by — sort column
order — "asc" or "desc"
Create-or-Get
Cell specs, instances, measurements, and parameterized
models support create_or_get() directly. If a resource
with the same name already exists, the existing one is
returned in a single round-trip:
spec = client.cell_spec.create_or_get({"name": "LGM50"})
pm = client.parameterized_model.create_or_get(spec.id, {"name": "Fit v1", ...})
For projects, studies, and models, create_or_get is not
yet available on the SDK. Use the duplicate-error fallback
pattern instead:
from ionworks import IonworksError
def create_or_get_project(client, data):
try:
return client.project.create(data)
except IonworksError as e:
if e.error_code == "CONFLICT" and e.data:
existing_id = e.data.get("detail", {}).get("existing_id")
if existing_id:
return client.project.get(existing_id)
raise
The backend includes existing_id in the error detail
on duplicates for projects, studies, and models, so the
fallback is reliable.
Related Skills
- manage-cells — detailed guide for cell specs, instances, measurements
- upload-data — measurement upload flows (time series, files, properties)
- run-simulations — running simulations with UCP protocols
- manage-projects — project, study, model, and optimization management
- ecm-fitting — fit Equivalent Circuit Models to cycling data and save as Parameterized Models
- electrolyte-transport — concentration-dependent electrolyte transport parameters from a material dataset