ionworks Python package
provides a programmatic interface for managing resources, running simulations,
submitting pipelines, and uploading data in Ionworks Studio.
Installation
ionworks requires Python 3.12 or later. It replaces the ionworks-api,
ionworks-schema, ionworksdata and iwutil packages, and cannot share an
environment with ionworks-api; if you have those installed, see the
migration guide.
Describing pipelines: ionworks.pipeline
ionworks.api submits work and collects results. To describe a fit or a
validation, use ionworks.pipeline, which
provides the typed building blocks — Pipeline, DataFit, objectives, costs,
and optimizers.
Authentication
Get your API key from the Ionworks account settings and configure it:API keys are valid for one year. The expiry date is shown next to your key
in account settings, and the page warns you when a key is within 30 days of
expiring. Regenerating a key resets its expiry and invalidates the
previous key, so update any integration that uses it. An expired key returns
401 with a message saying the key has expired.Importing
ionworks does not auto-load a .env file. Set IONWORKS_API_KEY in your shell
environment, load the .env file yourself (for example with
python-dotenv) before
constructing the client, or pass api_key= explicitly to iw.api.Client(...).Verify your API key
Useclient.whoami() to confirm which user and organization the configured
API key resolves to. This is the recommended way to debug 401/403 errors,
or to check why you’re seeing data from the wrong organization.
authorized_organization— the org this request is authorized as. For SDK calls, this is the org the configured API key was issued for, and is the source of truth for permission checks on every request the client makes. It’sNoneif no org context could be resolved.organizations— the user’s full membership list (every org they belong to). This is a different question and is not what permission checks run against.
id or name in authorized_organization doesn’t match what you
expect, the wrong key is in use — regenerate one for the correct org from
your account settings.
Default project
Most sub-clients (pipelines, studies, optimizations, cell specifications, …) operate within a project. Rather than threading aproject_id through every call, you can configure a default once
on the client. Sub-client methods that take a project_id argument fall back
to this default when one isn’t passed explicitly.
The client resolves the default in this order:
- The
project_id=argument passed toiw.api.Client(...). - The
IONWORKS_PROJECT_IDenvironment variable. - Otherwise, no default is set — methods that need a project will raise
ValueErrorunlessproject_idis passed at the call site.
https://app.ionworks.com/projects/<project-id>/settings.
The
PROJECT_ID environment variable is still accepted for backwards
compatibility but is deprecated. Set IONWORKS_PROJECT_ID instead — using
the old name emits a DeprecationWarning and will stop working in a future
release.Environment variables
The client reads the following variables from your shell environment when constructed..env files are not loaded automatically — populate the
environment yourself (for example by sourcing the file, or by calling
python-dotenv) before
constructing the client.
DataFrame backend
By default, the client returns data as polars DataFrames. You can switch to pandas if your workflow requires it.Timeout and retry behavior
The client automatically retries failed requests on connection errors, timeouts, and server errors (5xx). By default:- Requests time out after 10 seconds
- Failed requests retry up to 5 times with exponential backoff
- Dropped connections (the server closed the socket before the request reached the application — common on reused keep-alive connections) are retried for all methods, including POST and PATCH. The request never reached the server, so resending is safe.
- Read timeouts and 5xx responses are only retried for idempotent methods (GET and DELETE). The server may have already processed a POST or PATCH, so resubmitting could duplicate the operation.
Sub-clients
The client exposes domain-specific sub-clients:Searching across your organization
client.search.query() finds entities by name or description across your whole
organization in one call, without knowing which sub-client to reach for:
project, model, cell_specification,
cell_instance, cell_measurement, parameterized_model,
experiment_template, optimization_template, material, study,
optimization, and pipeline.
Unlike most sub-clients,
search does not fall back to the client’s
default project — it spans the whole organization unless you pass
project_id. Results are always limited to what your account can already
read.Discovering the API
client.capabilities() and client.schema(name) describe the API from the
API itself. Use them from a notebook or script to introduce yourself — or a
coding agent you’re scripting against — to the platform’s data hierarchy and
to fetch the authoritative JSON Schemas for measurements and UCP protocols.
capabilities() also returns pointers to the OpenAPI spec
(/openapi.json) and per-resource JSON Schema endpoints under
caps["schemas"], so agents can fetch the exact shape of any create/update
payload before calling it.
Linking to the web app
Every resource you fetch with.get() carries two links: url, its page in
the Ionworks web app (Ionworks Studio), and api_url, the API endpoint that
returns it. Use url to surface clickable links from notebooks, scripts,
dashboards, or Slack/email reports so collaborators can jump straight to the
resource.
client.simulation.get() and client.optimization.get() return dictionaries,
so read ["url"] instead of .url.
Only
.get() responses carry links. Items returned by .list() and the
responses from .create() do not — fetch the resource when you need its link.Building links from IDs
When you hold only an ID and want a link without an API request,client.urls
builds the same URLs locally. It is a fallback: prefer the url from .get(),
which is correct even when you don’t know the resource’s project.
project_id is optional — it falls back to the
default project configured on the client. The web-app host
is derived from the client’s API URL (api.ionworks.com → app.ionworks.com);
set IONWORKS_APP_URL to override it, for example in local development.
A simulation’s page is nested under its parameterized model. Pass
parameterized_model_id to build the link locally; without it,
client.urls.simulation() fetches the simulation and returns the url it
carries.
Canceling jobs
You can cancel running jobs (simulations, pipelines, and optimizations) using the Python API. Each job has a unique ID that you can use to cancel it.Resubmitting a failed pipeline
If a pipeline stops because its first failed element could not be submitted (error_code = SUBMISSION_FAILED), you can resubmit it without rebuilding the
configuration. Completed elements are preserved; execution resumes from the
failed element.
The Python client does not yet expose a resubmit method, so call the REST
endpoint directly as a temporary workaround. Resubmission goes through the
generic jobs endpoint, and a pipeline is identified by its job ID — the same
ID you pass to client.pipeline.get(...):
SUBMISSION_FAILED errors. For execution
timeouts or configuration mistakes, create a new pipeline with the corrected
configuration. For internal errors, wait briefly and create a new pipeline if
the issue persists. See Handling pipeline
failures for details.
Retrieving job metadata
Standard job-detail responses strip fields that can grow large — submitted configs, full result payloads, and serialized PyBaMM models — so listing and polling stay fast even for pipelines or optimizations with heavy inline data. When you actually need those fields, fetch them withclient.job.get_metadata:
- Re-run a job from its original submitted config without keeping a local copy.
- Audit the exact inputs that produced a result.
- Read large result blobs — for example, optimization traces, or the
validation_plot_configproduced by pipeline validations — that aren’t included in the default job response.
A validation element’s time series are not on this blob. They are stored
per objective, and
validation_series_index is the list of what exists. The
normal way to read them is off the typed result object —
client.pipeline.result(pipeline_id).element("validate").series returns
{objective: {source: {channel: [values]}}} at full resolution — source
being "optimal" or "baseline" — and .overlay returns the decimated data
behind the plots. Drop to
client.pipeline.get_element_series_channels and
client.pipeline.get_element_series when you want to fetch one named
objective instead of every sample of every objective.dict parsed from the job’s
metadata.json.gz blob. If a job has no metadata file (for example, it
failed before writing one) the call raises an error — wrap it in a
try/except if you’re iterating over many jobs.
Retrieving MCMC posterior samples
Data-fit jobs that use a sampler (for example,PintsSampler) evaluate
many parameter vectors rather than converging on a single point
estimate. The resulting chain is too large for the standard job result
column, so it is offloaded to the job’s metadata blob.
The raw chain is retrieved with client.job.get_posterior_samples:
samples— the chain as adict[str, list], keyed by parameter name. Each value is a nested list of shape(starts, iterations)for a multistart fit, and a flat list of lengthiterationswhen the fit ran a single start. Index the iteration axis last ([..., burnin:]) so both shapes work.sample_costs— the objective value at each sample, in the same shape as one parameter’s chain.sample_param_names— the parameter names in column order.sample_burnin— the number of initial iterations the sampler treats as burn-in. The chain includes them; discard them before downstream analysis. This isNonefor samplers that have no burn-in concept (GridSearch,PointEstimateSampler), so guard against it (e.g.burnin = samples["sample_burnin"] or 0) before using it as a slice boundary.
GridSearch and PointEstimateSampler runs also return a populated
chain, with sample_burnin set to None. Fits driven by a conventional
optimizer (CMAES, ScipyMinimize, and friends), along with
optimization and validation jobs, return an empty dict.
An empty dict also comes back when there is no metadata blob to read —
a job that failed before writing one, an unknown job_id, or a job
belonging to another organization all surface as “no samples” rather
than an error. Check that the job exists and completed before reading
an empty result as “this fit produced no chain”.