Running shadow tests¶
Info
Learn the concepts and fundamentals of shadow tests in the Explanation page.
A shadow test is an experiment that runs in the background and compares the results of two instances: baseline vs. candidate. When the shadow test has started, any run made on the baseline instance will trigger a run on the candidate instance using the same input and options. The results of the shadow test are often used to determine if a new version of a model is ready to be promoted to production.
Included in this how-to guide are instructions on how to interact with shadow
tests using methods on the Application class. Go
the reference section to see all the available parameters for each method.
Create a shadow test¶
Use the Application.new_shadow_test method to create
a new shadow test for an application. A shadow test requires comparisons to be
defined, via the comparisons keyword argument. Additionally, you must
specify the maximum number of runs that the test will run for, using the
termination_events keyword argument as a
TerminationEvents object with the
maximum_runs attribute.
Comparisons are defined as a dictionary. The keys of the comparisons
dictionary are the baseline instance IDs, and the values are the candidate
lists of instance IDs to compare against the respective baseline. Consider the
following example for doing a simple comparison of the staging vs.
production instances:
Here is an example that uses the comparison defined above to create a new shadow test for an application.
import os
import nextmv
from nextmv import cloud
from nextmv.cloud import TerminationEvents
client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = cloud.Application.get(client=client, id="uncanny-rodent")
shadow_test = app.new_shadow_test(
comparisons={"production": ["staging"]},
termination_events=TerminationEvents(maximum_runs=10),
)
nextmv.write(shadow_test.to_dict())
{
"id": "shadow-dm77fy97",
"name": "shadow-dm77fy97",
"description": "",
"app_id": "uncanny-rodent",
"created_at": "2026-07-29T04:03:41.591708Z",
"updated_at": "2026-07-29T04:03:41.591708Z",
"status": "draft",
"comparisons": [
{
"baseline_instance_id": "production",
"candidate_instance_ids": [
"staging"
]
}
],
"start_events": {},
"termination_events": {
"maximum_runs": 10
}
}
The call above will create a random ID, and use the same identifier for the
shadow test's name. The name of the test is used as a human-readable label.
You can use the shadow_test_id and/or name keyword arguments to specify a
custom ID and name for the shadow test. For example:
import os
import nextmv
from nextmv import cloud
from nextmv.cloud import TerminationEvents
client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = cloud.Application.get(client=client, id="uncanny-rodent")
shadow_test = app.new_shadow_test(
comparisons={"production": ["staging"]},
termination_events=TerminationEvents(maximum_runs=10),
shadow_test_id="giant-shadow-test",
name="Giant Shadow Test",
)
nextmv.write(shadow_test.to_dict())
{
"id": "giant-shadow-test",
"name": "Giant Shadow Test",
"description": "",
"app_id": "uncanny-rodent",
"created_at": "2026-07-29T04:03:46.649096Z",
"updated_at": "2026-07-29T04:03:46.649096Z",
"status": "draft",
"comparisons": [
{
"baseline_instance_id": "production",
"candidate_instance_ids": [
"staging"
]
}
],
"start_events": {},
"termination_events": {
"maximum_runs": 10
}
}
The Application.new_shadow_test method creates a
shadow test in draft mode. Once the test is created, you must start
it.
Start a shadow test¶
As mentioned above, when a shadow test is created, it is in draft mode. A shadow test must be started so that it can begin executing runs and collecting data. There are two ways to start a shadow test:
- Using the
start_eventskeyword argument (aStartEventsobject) with theApplication.new_shadow_testmethod. This will start the shadow test at the scheduled time. - Using the
Application.start_shadow_testmethod. This will start the shadow test as soon as the method is called.
As an example, here is how to start a shadow test using the
Application.start_shadow_test method:
Once a shadow test has started, you can stop it at any time or wait for it to complete based on the termination events defined when the test was created.
Get a shadow test¶
Info
The best way to view and interact with shadow test results is in the Nextmv Console.
Use the Application.shadow_test_metadata method
to retrieve the metadata for a shadow test, using the shadow test ID. The
method returns a ShadowTestMetadata
object.
Once the status of the shadow test is completed, you can get the results
using the Application.shadow_test method. The method
returns a ShadowTest object, whose output includes
the runs that were made for the test. If the test hasn't completed, you can
still get partial results.
A shadow test that has already started can be stopped at any time, or
it will stop automatically when the termination events are fulfilled. Once a
shadow test is stopped, then its status will move to completed.
{
"id": "giant-shadow-test",
"name": "Giant Shadow Test",
"description": "",
"app_id": "uncanny-rodent",
"created_at": "2026-07-29T04:03:46.649096Z",
"updated_at": "2026-07-29T04:04:21.873630Z",
"status": "started",
"comparisons": [
{
"baseline_instance_id": "production",
"candidate_instance_ids": [
"staging"
]
}
],
"start_events": {},
"termination_events": {
"maximum_runs": 10
},
"grouped_distributional_summaries": [
{
"group_keys": [
"instanceID",
"versionID"
],
"group_values": [
"staging",
"v0.0.2"
],
"indicator_keys": [
"value"
],
"indicator_distributions": {
"value": {
"min": 1.23,
"max": 1.23,
"count": 2,
"mean": 1.23,
"std": 0,
"shifted_geometric_mean": {
"value": 1.2300000000000022,
"shift": 10
},
"percentiles": {
"p01": 1.23,
"p05": 1.23,
"p10": 1.23,
"p25": 1.23,
"p50": 1.23,
"p75": 1.23,
"p90": 1.23,
"p95": 1.23,
"p99": 1.23
}
}
},
"number_of_runs_total": 2
},
{
"group_keys": [
"instanceID",
"versionID"
],
"group_values": [
"production",
"v0.0.2"
],
"indicator_keys": [
"value"
],
"indicator_distributions": {
"value": {
"min": 1.23,
"max": 1.23,
"count": 2,
"mean": 1.23,
"std": 0,
"shifted_geometric_mean": {
"value": 1.2300000000000022,
"shift": 10
},
"percentiles": {
"p01": 1.23,
"p05": 1.23,
"p10": 1.23,
"p25": 1.23,
"p50": 1.23,
"p75": 1.23,
"p90": 1.23,
"p95": 1.23,
"p99": 1.23
}
}
},
"number_of_runs_total": 2
}
],
"runs": [
{
"id": "production-8jV6UUPDg",
"user_email": "sebastian@nextmv.io",
"name": "",
"description": "",
"created_at": "2026-07-29T04:04:20.536496Z",
"application_id": "uncanny-rodent",
"application_instance_id": "production",
"application_version_id": "v0.0.2",
"run_type": {
"definition_id": "",
"reference_id": ""
},
"execution_class": "6c9500mb870s",
"runtime": "python-3_11",
"status_v2": "succeeded",
"queuing_priority": 6,
"queuing_disabled": true,
"metrics": {
"status": "succeeded",
"indicators": [
{
"name": "value",
"value": 1.23
},
{
"name": "metadata.duration",
"value": 4.377
}
]
},
"options": {
"details": "true"
},
"options_summary": [
{
"name": "details",
"value": "true",
"source": "version"
}
]
},
{
"id": "staging-YMSe8UPvR",
"user_email": "sebastian@nextmv.io",
"name": "",
"description": "",
"created_at": "2026-07-29T04:04:20.757240Z",
"application_id": "uncanny-rodent",
"application_instance_id": "staging",
"application_version_id": "v0.0.2",
"run_type": {
"definition_id": "",
"reference_id": ""
},
"execution_class": "6c9500mb870s",
"runtime": "python-3_11",
"status_v2": "succeeded",
"queuing_priority": 6,
"queuing_disabled": false,
"metrics": {
"status": "succeeded",
"indicators": [
{
"name": "value",
"value": 1.23
},
{
"name": "metadata.duration",
"value": 4.373
}
]
},
"options": {
"details": "true"
},
"request_options": {
"details": "true"
},
"options_summary": [
{
"name": "details",
"value": "true",
"source": "run"
}
]
},
{
"id": "production-imHe88EvR",
"user_email": "sebastian@nextmv.io",
"name": "",
"description": "",
"created_at": "2026-07-29T04:04:21.698655Z",
"application_id": "uncanny-rodent",
"application_instance_id": "production",
"application_version_id": "v0.0.2",
"run_type": {
"definition_id": "",
"reference_id": ""
},
"execution_class": "6c9500mb870s",
"runtime": "python-3_11",
"status_v2": "succeeded",
"queuing_priority": 6,
"queuing_disabled": true,
"metrics": {
"status": "succeeded",
"indicators": [
{
"name": "value",
"value": 1.23
},
{
"name": "metadata.duration",
"value": 4.456
}
]
},
"options": {
"details": "true"
},
"options_summary": [
{
"name": "details",
"value": "true",
"source": "version"
}
]
},
{
"id": "staging-9IHeU8Pvg",
"user_email": "sebastian@nextmv.io",
"name": "",
"description": "",
"created_at": "2026-07-29T04:04:21.884426Z",
"application_id": "uncanny-rodent",
"application_instance_id": "staging",
"application_version_id": "v0.0.2",
"run_type": {
"definition_id": "",
"reference_id": ""
},
"execution_class": "6c9500mb870s",
"runtime": "python-3_11",
"status_v2": "succeeded",
"queuing_priority": 6,
"queuing_disabled": false,
"metrics": {
"status": "succeeded",
"indicators": [
{
"name": "value",
"value": 1.23
},
{
"name": "metadata.duration",
"value": 5.81
}
]
},
"options": {
"details": "true"
},
"request_options": {
"details": "true"
},
"options_summary": [
{
"name": "details",
"value": "true",
"source": "run"
}
]
}
]
}
You can list all shadow tests in the application using the
Application.list_shadow_tests method.
[
{
"id": "giant-shadow-test",
"name": "Giant Shadow Test",
"description": "",
"app_id": "uncanny-rodent",
"created_at": "2026-07-29T04:03:46.649096Z",
"updated_at": "2026-07-29T04:04:21.873630Z",
"status": "started",
"comparisons": [
{
"baseline_instance_id": "production",
"candidate_instance_ids": [
"staging"
]
}
],
"start_events": {},
"termination_events": {
"maximum_runs": 10
}
},
{
"id": "shadow-dm77fy97",
"name": "shadow-dm77fy97",
"description": "",
"app_id": "uncanny-rodent",
"created_at": "2026-07-29T04:03:41.591708Z",
"updated_at": "2026-07-29T04:03:41.591708Z",
"status": "draft",
"comparisons": [
{
"baseline_instance_id": "production",
"candidate_instance_ids": [
"staging"
]
}
],
"start_events": {},
"termination_events": {
"maximum_runs": 10
}
}
]
Stop a shadow test¶
A shadow test will continue to execute until it stops, which is equivalent to it being completed. There are two ways to stop a shadow test:
- Using the
termination_eventskeyword argument with theApplication.new_shadow_testmethod. Termination events will cause the shadow test to stop. - Using the
Application.stop_shadow_testmethod. This will stop the shadow test as soon as the method is called.
When using the Application.stop_shadow_test method, you must use the
intent keyword argument (a StopIntent value) to
track if you want to complete or cancel the shadow test. As an example, here
is how to stop a shadow test and mark it as completed:
Update a shadow test¶
You can update attributes of a shadow test with the
Application.update_shadow_test method, such as
its:
- Name
- Description
The method returns a ShadowTest object. You cannot update the ID of a
shadow test.
import os
import nextmv
from nextmv import cloud
client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
app = cloud.Application.get(client=client, id="uncanny-rodent")
shadow_test = app.update_shadow_test(
shadow_test_id="giant-shadow-test",
name="Updated Shadow Test Name",
description="Updated description for the shadow test",
)
nextmv.write(shadow_test.to_dict())
Delete a shadow test¶
Warning
Deleting a shadow test is irreversible. All the data associated with the shadow test will be permanently deleted.
Delete a shadow test using the
Application.delete_shadow_test method.