From ionworks
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".
How this skill is triggered — by the user, by Claude, or both
Slash command
/ionworks:discover-apiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Before making any request to the Ionworks API, fetch
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.
Use this skill when the user:
Do NOT use this skill for:
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() # reads IONWORKS_API_KEY from env
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:
# Cell data hierarchy (specs, instances, measurements,
# columns, metadata):
schema = client.schema("data")
# Universal Cycler Protocol (UCP) for simulations:
schema = client.schema("protocol")
# Per-resource request/response shapes:
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.
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.
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-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), .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. Reference only — no worked example in a skill yet. |
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 (direct) | .capabilities(), .schema(name) where name is "data", "protocol", "project", "study", "model", "parameterized_model", "optimization", or "pipeline", .health_check(), .whoami() |
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")
Use column names, field names, and entity structures from the responses you already fetched — do not hardcode or guess them.
organization
-> project
-> cell_specification
-> cell_instance
-> cell_measurement
-> [time_series, steps, cycles]
-> parameterized_model
-> study
-> simulation_mappings
-> measurement_mappings
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)
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() # first page only
for spec in specs:
print(spec.name)
page = client.cell_spec.list(limit=10, offset=0)
page.total # total matching records across all pages
page.count # number of items in this page
# Full scan — page until exhausted
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)
Cell specs, instances, and measurements support keyword-only filter parameters:
specs = client.cell_spec.list(
name="LGM50", # case-insensitive substring match
name_exact="LGM50 Demo", # exact match (precedence over name)
form_factor="cylindrical",
created_by_email="user@",
created_after="2025-01-01",
order_by="name", # name, created_at, updated_at
order="asc", # asc or desc
)
Common filter parameters across cell list methods:
name — case-insensitive substring matchname_exact — exact matchcreated_by_email — substring match on creator emailcreated_after / created_before — date rangeupdated_after / updated_before — date rangeorder_by — sort columnorder — "asc" or "desc"Cell specs, instances, and measurements 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"})
# Returns existing spec if "LGM50" already exists
For projects, studies, models, and parameterized 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, models, and
parameterized models, so the fallback is reliable.
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub ionworks/ionworks-skills --plugin ionworks