Deploy a container image and get a URL that keeps working whether or not anything is running behind it. A sandbox scales from zero on requests, and can also be started by game events — a player joining, a server going quiet — through triggers, so you write the code that runs when something happens and nothing else. Creating a sandbox returns at once with a state to watch; every change to its template is a new revision, and a failed deploy leaves the previous one serving.
Creates a sandbox and mints its first revision.
Returns immediately with STATE_PROVISIONING. The URL is assigned here and
never changes, so it can be written into a client before anything has
finished starting — which is the ordinary case for a terraform apply that
wires a sandbox to something else in the same plan.
parentstringRequiredprojects/{project}/locations/{location}.
What to deploy.
Writing this mints a revision. Reading it gives back what was last written,
which is not necessarily what is serving: a revision that failed to bake
leaves serving_revision on the one before it, and the difference between
the two fields is exactly "a deploy that has not landed".
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
commandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
namestringIdentifierprojects/{project}/locations/{location}/sandboxes/{sandbox}.
Regional. The image is in one region's object store, the instances are on one region's hosts, and the URL names the region — a customer wanting two regions has a sandbox in each, which is the honest arrangement rather than a global name whose availability quietly depends on a region they never chose.
display_namestringWhat a person calls it. Free text.
descriptionstringlabelsmap<string, string>scalingScalingSpecmin_instancesint32Instances to keep running even with no traffic. Defaults to 0.
Zero is scale-to-zero. One or more is a container that runs permanently, and is charged for continuously.
max_instancesint32Ceiling. Defaults to 10.
A ceiling rather than a target, and the thing standing between a runaway
trigger and a bill. It is also what
sandbox.plany.cloud/max_instances is counted in, so it cannot be raised
past what the project's quota allows.
idle_timeoutDurationHow long an instance may sit with nothing in flight before it is released. Defaults to 15 minutes.
Measured from the last request finishing, as counted by Plany in front of the container — the container is never asked whether it is busy.
ingressIngressINGRESS_PUBLICINGRESS_INTERNALserving_revisionstringThe revision serving traffic, or empty to serve the newest READY one.
Setting it is a rollback, and it is a rollback rather than a redeploy on purpose: the bytes are already baked, so it takes effect in the time it takes to start an instance rather than the time it takes to pull an image that may no longer exist under the tag it was pushed with.
service_accountstringaccounts/{account}/serviceAccounts/{service_account}.
The identity the container runs as, and what it pulls its image and reads its secrets with. Left empty, Plany provisions one, binds it narrowly, and revokes it on delete — the derived-name arrangement Playpute uses, which is what lets a teardown tell an identity the platform made from one a customer attached.
Set it to attach an identity of your own, which requires
iam.serviceAccounts.actAs on it. Plany grants a customer-supplied
identity nothing: whatever it can reach, it could already reach.
annotationsmap<string, string>sandbox_idstringRequiredThe last segment of the name. AIP-122: lower case, digits and hyphens.
It appears in the URL, so it is also a DNS label: at most 40 characters, and not ending in a hyphen.
request_idstringAn idempotency key. A retry with the same value returns the sandbox the first call created rather than colliding.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations/", "sandbox": { "display_name": "My server", "template": { "image": "", "resources": { "cpu": {}, "memory": {} } } }, "sandbox_id": "my-sandbox" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/CreateSandbox{ "name": "projects//locations//sandboxes/", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "description": "", "labels": { "key": "" }, "template": { "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "secrets": [ { "secret": "projects//locations//secrets/", "environment_variable": "", "file_path": "" } ], "port": 0, "protocol": "PROTOCOL_HTTP1", "resources": {}, "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/" }, "scaling": { "min_instances": 0, "max_instances": 0, "idle_timeout": "300s" }, "ingress": "INGRESS_PUBLIC", "serving_revision": "projects//locations//sandboxes//revisions/my-revision", "latest_revision": "projects//locations//sandboxes//revisions/my-revision", "url": "", "service_account": "accounts//serviceAccounts/my-service-account", "state": "STATE_PROVISIONING", "state_reason": "", "instance_count": 0, "has_instance_count": false, "annotations": { "key": "" }, "generation": "0", "observed_generation": "0", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\""}grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes/" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/GetSandbox{ "name": "projects//locations//sandboxes/", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "description": "", "labels": { "key": "" }, "template": { "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "secrets": [ { "secret": "projects//locations//secrets/", "environment_variable": "", "file_path": "" } ], "port": 0, "protocol": "PROTOCOL_HTTP1", "resources": {}, "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/" }, "scaling": { "min_instances": 0, "max_instances": 0, "idle_timeout": "300s" }, "ingress": "INGRESS_PUBLIC", "serving_revision": "projects//locations//sandboxes//revisions/my-revision", "latest_revision": "projects//locations//sandboxes//revisions/my-revision", "url": "", "service_account": "accounts//serviceAccounts/my-service-account", "state": "STATE_PROVISIONING", "state_reason": "", "instance_count": 0, "has_instance_count": false, "annotations": { "key": "" }, "generation": "0", "observed_generation": "0", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\""}parentstringRequiredpage_sizeint32page_tokenstringfilterstringAIP-160, over display_name, labels and state.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations/", "page_size": 50 }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/ListSandboxes{ "sandboxes": [ { "name": "projects//locations//sandboxes/", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "description": "", "labels": { "key": "" }, "template": { "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "port": 0, "protocol": "PROTOCOL_HTTP1", "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/" }, "scaling": { "min_instances": 0, "max_instances": 0, "idle_timeout": "300s" }, "ingress": "INGRESS_PUBLIC", "serving_revision": "projects//locations//sandboxes//revisions/my-revision", "latest_revision": "projects//locations//sandboxes//revisions/my-revision", "url": "", "service_account": "accounts//serviceAccounts/my-service-account", "state": "STATE_PROVISIONING", "state_reason": "", "instance_count": 0, "has_instance_count": false, "annotations": { "key": "" }, "generation": "0", "observed_generation": "0", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\"" } ], "next_page_token": ""}Updates a sandbox. Changing template deploys a new revision.
template is replaced as a whole when it is in the update mask, not merged: send only a new image and the environment, secrets and resources are dropped. Read the current template, change what you need, and send all of it.
What to deploy.
Writing this mints a revision. Reading it gives back what was last written,
which is not necessarily what is serving: a revision that failed to bake
leaves serving_revision on the one before it, and the difference between
the two fields is exactly "a deploy that has not landed".
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
commandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
namestringIdentifierprojects/{project}/locations/{location}/sandboxes/{sandbox}.
Regional. The image is in one region's object store, the instances are on one region's hosts, and the URL names the region — a customer wanting two regions has a sandbox in each, which is the honest arrangement rather than a global name whose availability quietly depends on a region they never chose.
display_namestringWhat a person calls it. Free text.
descriptionstringlabelsmap<string, string>scalingScalingSpecmin_instancesint32Instances to keep running even with no traffic. Defaults to 0.
Zero is scale-to-zero. One or more is a container that runs permanently, and is charged for continuously.
max_instancesint32Ceiling. Defaults to 10.
A ceiling rather than a target, and the thing standing between a runaway
trigger and a bill. It is also what
sandbox.plany.cloud/max_instances is counted in, so it cannot be raised
past what the project's quota allows.
idle_timeoutDurationHow long an instance may sit with nothing in flight before it is released. Defaults to 15 minutes.
Measured from the last request finishing, as counted by Plany in front of the container — the container is never asked whether it is busy.
ingressIngressINGRESS_PUBLICINGRESS_INTERNALserving_revisionstringThe revision serving traffic, or empty to serve the newest READY one.
Setting it is a rollback, and it is a rollback rather than a redeploy on purpose: the bytes are already baked, so it takes effect in the time it takes to start an instance rather than the time it takes to pull an image that may no longer exist under the tag it was pushed with.
service_accountstringaccounts/{account}/serviceAccounts/{service_account}.
The identity the container runs as, and what it pulls its image and reads its secrets with. Left empty, Plany provisions one, binds it narrowly, and revokes it on delete — the derived-name arrangement Playpute uses, which is what lets a teardown tell an identity the platform made from one a customer attached.
Set it to attach an identity of your own, which requires
iam.serviceAccounts.actAs on it. Plany grants a customer-supplied
identity nothing: whatever it can reach, it could already reach.
annotationsmap<string, string>Which fields to write.
display_name, description, labels, annotations, template,
scaling, ingress and serving_revision. Naming template mints a
revision and replaces the whole message — see the method comment.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "sandbox": { "display_name": "My server", "template": { "image": "", "resources": { "cpu": {}, "memory": {} } } }, "update_mask": "display_name" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/UpdateSandbox{ "name": "projects//locations//sandboxes/", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "description": "", "labels": { "key": "" }, "template": { "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "secrets": [ { "secret": "projects//locations//secrets/", "environment_variable": "", "file_path": "" } ], "port": 0, "protocol": "PROTOCOL_HTTP1", "resources": {}, "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/" }, "scaling": { "min_instances": 0, "max_instances": 0, "idle_timeout": "300s" }, "ingress": "INGRESS_PUBLIC", "serving_revision": "projects//locations//sandboxes//revisions/my-revision", "latest_revision": "projects//locations//sandboxes//revisions/my-revision", "url": "", "service_account": "accounts//serviceAccounts/my-service-account", "state": "STATE_PROVISIONING", "state_reason": "", "instance_count": 0, "has_instance_count": false, "annotations": { "key": "" }, "generation": "0", "observed_generation": "0", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\""}Deletes a sandbox, its revisions, its triggers and its instances.
The URL stops answering immediately. A service account you attached is left alone.
namestringRequiredetagstringRefuse if the sandbox has changed since this etag was read.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes/" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/DeleteSandbox{}namestringRequiredgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes//revisions/my-revision" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/GetSandboxRevision{ "name": "projects//locations//sandboxes//revisions/my-revision", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "template": { "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "secrets": [ { "secret": "projects//locations//secrets/", "environment_variable": "", "file_path": "" } ], "port": 0, "protocol": "PROTOCOL_HTTP1", "resources": {}, "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/" }, "image_digest": "", "schematic": "", "state": "STATE_PROVISIONING", "state_reason": "", "created_by": "", "create_time": "2026-10-04T12:00:00Z", "ready_time": "2026-10-04T12:00:00Z"}Lists revisions, newest first. What a rollback picks from.
parentstringRequiredpage_sizeint32page_tokenstringgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations//sandboxes/", "page_size": 50 }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/ListSandboxRevisions{ "revisions": [ { "name": "projects//locations//sandboxes//revisions/my-revision", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "template": { "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "port": 0, "protocol": "PROTOCOL_HTTP1", "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/" }, "image_digest": "", "schematic": "", "state": "STATE_PROVISIONING", "state_reason": "", "created_by": "", "create_time": "2026-10-04T12:00:00Z", "ready_time": "2026-10-04T12:00:00Z" } ], "next_page_token": ""}Deletes a revision and the disk baked for it.
Refused for the revision currently serving, and for the newest READY one — deleting the thing a rollback would land on is a decision made once and regretted under pressure.
namestringRequiredgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes//revisions/my-revision" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/DeleteSandboxRevision{}parentstringRequiredprojects/{project}/locations/{location}/sandboxes/{sandbox}.
What to deliver.
EVENT_TYPE_CONNECTION_OPENEDEVENT_TYPE_CONNECTION_CLOSEDEVENT_TYPE_PLAYER_JOINEDEVENT_TYPE_PLAYER_LEFTEVENT_TYPE_SERVER_READYEVENT_TYPE_SERVER_STOPPINGnamestringIdentifierdisplay_namestringgame_serverstringDeliver only events about this game server.
projects/{project}/locations/{location}/gameServers/{game_server}, or
empty for every game server in the project. The narrow case is the common
one — a trigger for a specific fleet — and the wide one is what a project's
analytics wants.
pathstringThe request path events are delivered to. Defaults to /.
So one sandbox can serve several triggers and route on the path, which is how an ordinary web framework already wants to be written.
stateSandboxTrigger.StateSTATE_ACTIVESTATE_PAUSEDtrigger_idstringRequiredgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations//sandboxes/", "trigger": { "display_name": "My server", "event_type": "EVENT_TYPE_CONNECTION_OPENED" }, "trigger_id": "my-trigger" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/CreateSandboxTrigger{ "name": "projects//locations//sandboxes//triggers/my-trigger", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "event_type": "EVENT_TYPE_CONNECTION_OPENED", "game_server": "", "path": "", "state": "STATE_ACTIVE", "failed_delivery_count": "0", "last_delivery_time": "2026-10-04T12:00:00Z", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\""}namestringRequiredgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes//triggers/my-trigger" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/GetSandboxTrigger{ "name": "projects//locations//sandboxes//triggers/my-trigger", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "event_type": "EVENT_TYPE_CONNECTION_OPENED", "game_server": "", "path": "", "state": "STATE_ACTIVE", "failed_delivery_count": "0", "last_delivery_time": "2026-10-04T12:00:00Z", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\""}parentstringRequiredpage_sizeint32page_tokenstringgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations//sandboxes/", "page_size": 50 }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/ListSandboxTriggers{ "triggers": [ { "name": "projects//locations//sandboxes//triggers/my-trigger", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "event_type": "EVENT_TYPE_CONNECTION_OPENED", "game_server": "", "path": "", "state": "STATE_ACTIVE", "failed_delivery_count": "0", "last_delivery_time": "2026-10-04T12:00:00Z", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\"" } ], "next_page_token": ""}What to deliver.
EVENT_TYPE_CONNECTION_OPENEDEVENT_TYPE_CONNECTION_CLOSEDEVENT_TYPE_PLAYER_JOINEDEVENT_TYPE_PLAYER_LEFTEVENT_TYPE_SERVER_READYEVENT_TYPE_SERVER_STOPPINGnamestringIdentifierdisplay_namestringgame_serverstringDeliver only events about this game server.
projects/{project}/locations/{location}/gameServers/{game_server}, or
empty for every game server in the project. The narrow case is the common
one — a trigger for a specific fleet — and the wide one is what a project's
analytics wants.
pathstringThe request path events are delivered to. Defaults to /.
So one sandbox can serve several triggers and route on the path, which is how an ordinary web framework already wants to be written.
stateSandboxTrigger.StateSTATE_ACTIVESTATE_PAUSEDdisplay_name, event_type, game_server, path and state.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "trigger": { "display_name": "My server", "event_type": "EVENT_TYPE_CONNECTION_OPENED" }, "update_mask": "display_name" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/UpdateSandboxTrigger{ "name": "projects//locations//sandboxes//triggers/my-trigger", "uid": "0b6f3c1e-8a2d-4f6e-9c1a-2f4b7d9e0a11", "display_name": "My server", "event_type": "EVENT_TYPE_CONNECTION_OPENED", "game_server": "", "path": "", "state": "STATE_ACTIVE", "failed_delivery_count": "0", "last_delivery_time": "2026-10-04T12:00:00Z", "create_time": "2026-10-04T12:00:00Z", "update_time": "2026-10-04T12:00:00Z", "etag": "W/\"1\""}namestringRequiredgrpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes//triggers/my-trigger" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/DeleteSandboxTrigger{}Lists deliveries that were abandoned after exhausting their retries.
The one place a customer finds out that their code did not run. A trigger whose sandbox has been failing for an hour is otherwise indistinguishable from a quiet evening, and a dropped player-join is business logic that did not happen.
parentstringRequiredprojects/{project}/locations/{location}/sandboxes/{sandbox}.
page_sizeint32page_tokenstringtriggerstringOnly deliveries for this trigger.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "", "page_size": 50 }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/ListFailedDeliveries{ "deliveries": [ { "trigger": "", "event_type": "EVENT_TYPE_CONNECTION_OPENED", "event_id": "my-event", "game_server": "", "reason": "", "attempts": 0, "event_time": "2026-10-04T12:00:00Z", "last_attempt_time": "2026-10-04T12:00:00Z" } ], "next_page_token": ""}Reports how a revision's deploy ended, from inside the machine installing its image.
Called by Plany's own agent on that machine — you do not call it. It is how a revision deployed from a tag in another registry comes to show the digest the tag resolved to, and how an image that can never be installed — a refused credential, a tag that does not exist, an image with no linux/amd64 variant — fails the deploy at once with that reason in state_reason.
namestringRequiredThe revision being baked:
projects/{project}/locations/{location}/sandboxes/{sandbox}/revisions/{revision}.
name rather than revision, as on every other method that acts on one
existing resource — the field every reflection-based gate on the platform
looks for to find whose resource a call is about.
bake_tokenstringRequiredA one-time value given only to the machine installing this revision, and refused once the deploy has finished.
image_digeststringOne of outcomeThe digest the image's tag resolved to, sha256: and 64 lower-case hex characters. Recorded only if the revision has no digest yet; a different digest from the one recorded is refused, because it means the tag moved during the deploy.
failure_reasonstringOne of outcomeWhy the image cannot be installed, in one sentence. Fails the revision.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//sandboxes//revisions/my-revision", "bake_token": "" }' \ api.plany.cloud:443 plany.sandbox.v1.SandboxService/ReportBakeResult{}FailedDelivery is one event a sandbox never successfully received.
triggerstringevent_typeEventTypeEVENT_TYPE_CONNECTION_OPENEDEVENT_TYPE_CONNECTION_CLOSEDEVENT_TYPE_PLAYER_JOINEDEVENT_TYPE_PLAYER_LEFTEVENT_TYPE_SERVER_READYEVENT_TYPE_SERVER_STOPPINGevent_idstringThe event's id, to quote when asking Plany support about it.
game_serverstringWhat the game server was, when the event named one.
reasonstringThe last failure, in words. An HTTP status, a timeout, a sandbox that could not be started.
attemptsint32event_timeTimestamplast_attempt_timeTimestamp{ "trigger": "", "event_type": "EVENT_TYPE_CONNECTION_OPENED", "event_id": "my-event", "game_server": "", "reason": "", "attempts": 0, "event_time": "2026-10-04T12:00:00Z", "last_attempt_time": "2026-10-04T12:00:00Z"}SandboxTemplate is what a revision is made of.
Every field here is part of the revision's identity: changing any of them mints a new revision, because a container started with a different environment is a different deployment and pretending otherwise makes a rollback restore something the customer did not have.
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
vcpusint32RequiredNumber of virtual CPUs the workload sees. Required, must be at least 1.
reserved_millisint64CPU time reserved on the host, in millicores (1000 = one full core).
Must be <= vcpus * 1000. Zero means "reserve the full vcpus * 1000", i.e. no oversubscription. Below that, the VM is burstable and the difference is the burst headroom.
limit_millisint64Hard ceiling on host CPU time, in millicores, enforced by the agent via cgroup cpu.max. Zero means the ceiling is vcpus * 1000. Set this below vcpus * 1000 to cap a burstable VM that would otherwise consume all idle capacity on the node.
architectureCpuArchitectureCPU_ARCHITECTURE_X86_64CPU_ARCHITECTURE_ARM64required_featuresrepeated stringCPUID feature flags the workload requires, lowercase as they appear in /proc/cpuinfo — e.g. "avx2", "aes". Placement rejects nodes lacking any of them. Anti-cheat and DRM in game servers occasionally depend on these.
dedicated_coresboolPin each vCPU to a distinct physical core and let no other workload share those cores. Removes noisy-neighbour jitter for latency-sensitive tick loops, at the cost of a dedicated-core price.
isolate_smt_siblingsboolRequire SMT siblings of the pinned cores to stay idle. Only meaningful with dedicated_cores; used for workloads that are sensitive to hyperthread contention or that need side-channel isolation.
size_bytesint64RequiredMemory the workload sees, in bytes. Required.
reserved_bytesint64Memory guaranteed to the workload, in bytes. Must be no more than size_bytes. Zero means all of size_bytes is guaranteed.
Reserving less than size_bytes requires balloon_enabled, so unused memory can be reclaimed when it is needed elsewhere.
balloon_enabledboolAllow memory the workload is not using to be reclaimed. Required when reserved_bytes is less than size_bytes.
hugepagesboolBack the workload's memory with 2 MiB pages. Noticeably faster for memory-heavy servers — large voxel worlds, big entity counts — but can only be placed where such memory is available.
hotplug_max_bytesint64Allow memory to grow to this size while running, without a restart. Zero disables it. Must be at least size_bytes when set.
sharedboolAllow identical memory pages to be shared with other workloads on the same machine. Saves memory when many servers run the same game, at some CPU cost and with weaker isolation between them.
disksrepeated DiskSpecBlock devices, in attachment order. Exactly one must set boot unless the
workload boots from an initramfs alone.
disk_idstringRequiredIdentifier for this disk within the workload, e.g. "root" or "world-data". Must be unique within it.
size_bytesint64Size of the device. Required for a blank disk; for an image or snapshot source, zero means "the source's own size" and any larger value grows the filesystem on first boot.
typeDiskTypeDISK_TYPE_LOCAL_NVMEDISK_TYPE_NETWORK_SSDDISK_TYPE_NETWORK_SSD_HIGH_IOPSread_onlyboolAttach read-only. Several instances may share one read-only volume, which is how a common asset bundle is mounted into many game servers at once.
bootboolMarks the disk the workload boots from. Exactly one disk may set this.
delete_on_instance_deleteboolDelete the underlying storage when the instance is deleted. Always true in effect for LOCAL_NVME.
io_limitsDiskSpec.IoLimitsdirect_ioboolUse direct IO (O_DIRECT), bypassing the host page cache. Lowers memory pressure on the node and makes latency more predictable, at the cost of losing host-side caching.
network_interfacesrepeated NetworkInterfaceSpecinterface_idstringRequiredStable identifier within the instance, e.g. "eth0".
networkstringPrivate network to attach to, as a resource name. Empty means the project's default network in this location.
private_ipv4stringRequested private IPv4 address. Empty allocates one from the subnet.
public_address_modeNetworkInterfaceSpec.PublicAddressModePUBLIC_ADDRESS_MODE_NONEPUBLIC_ADDRESS_MODE_SHARED_IPV4PUBLIC_ADDRESS_MODE_DEDICATED_IPV4PUBLIC_ADDRESS_MODE_IPV6tierNetworkTierNETWORK_TIER_STANDARDNETWORK_TIER_PREMIUMrate_limitsNetworkInterfaceSpec.RateLimitsmac_addressstringThe interface's MAC address. Empty generates one. Pinning it matters for game licences tied to a MAC.
multi_queueboolEnable multi-queue virtio-net with one queue pair per vCPU. Raises packet throughput for servers handling many concurrent clients.
tagsrepeated stringNetwork tags, which firewall rules on this interface's network target.
Per interface rather than per instance because a rule belongs to one network, and an instance with an interface on each of two networks is answerable to two sets of rules. Lowercase letters, digits and hyphens, up to 63 characters each.
deny_inboundboolRefuse every inbound connection on this interface, whatever the network's firewall rules say. Replies to connections the workload opened still arrive.
For an interface that exists so a workload can reach a network rather than be reached on it — a sandbox connected to one of your networks, for example.
no_default_routeboolSend no internet traffic and no DNS through this interface: only the network's own range is reached through it. Without this, a workload with two interfaces could send its internet traffic out of either.
placementPlacementSpeczonesrepeated stringZones the instance may land in, as location ids ("eu-west-2a"). Empty means any zone in the region. More than one entry is a preference list in priority order, not a hard set.
required_node_selectorsrepeated NodeSelectorRequirementHard node requirements. A node must satisfy every entry.
preferred_node_selectorsrepeated NodeSelectorRequirementSoft node preferences. Nodes satisfying more of these score higher, but none of them can make placement fail.
placement_groupstringGroup this workload belongs to, scoped to the project. Workloads sharing a group are spread out or packed together according to policy.
policyPlacementPolicyPLACEMENT_POLICY_PACKPLACEMENT_POLICY_SPREADPLACEMENT_POLICY_STRICT_SPREAD_NODEPLACEMENT_POLICY_STRICT_SPREAD_ZONEcapacity_classstringClass of capacity to draw from. Leave empty to let Plany choose, which is right for almost every workload.
disallow_preemptibleboolRefuse to place onto pre-emptible (spot) capacity. Spot is materially cheaper and fine for a dev server, but wrong for a ranked match in progress.
nodestringPin to one node by resource name. Bypasses scoring entirely; intended for debugging and for rescheduling a VM back onto the node holding its local disk.
schedulingSchedulingSpecon_host_maintenanceSchedulingSpec.OnHostMaintenanceON_HOST_MAINTENANCE_MIGRATEON_HOST_MAINTENANCE_TERMINATErestart_policySchedulingSpec.RestartPolicyRESTART_POLICY_NEVERRESTART_POLICY_ALWAYSRESTART_POLICY_ON_FAILUREpreemptibleboolRun as spot: on spare capacity, at the spot rate, reclaimable with two minutes' notice when an on-demand workload needs the room.
preemption_noticeDurationWarning given before a spot reclaim, readable at /metadata/spot/instance-action on the metadata service. The notice is always two minutes; leave this unset.
max_run_durationDurationStop the instance automatically once it has run this long. Zero means no limit. This is the guard rail that stops a forgotten dev server draining a hobby account's credit overnight.
termination_grace_periodDurationTime between the shutdown signal and a forced stop, so a game server can save its world. Zero uses the default of thirty seconds.
preemptible_fallbackboolWith preemptible: place as an ordinary on-demand workload when no spare
capacity fits, rather than waiting for some. The allocation says which
happened, and it is what the instance is billed at.
commandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
secretstringRequiredprojects/{project}/locations/{location}/secrets/{secret}, optionally with
/versions/{version}.
Unqualified resolves to the latest enabled version, so a rotation reaches this container on its next start with nothing to redeploy. Pin a version only when a revision must not follow a rotation.
environment_variablestringRequiredThe environment variable the container sees it as.
Upper case, digits and underscores, not starting with a digit. The payload must be valid UTF-8 to be injected this way; a binary key is refused at launch rather than silently mangled.
file_pathstringAlso write the value to this absolute path inside the container.
For the things that will not go in a variable: a service account JSON, a TLS key, a licence file something reads by path. Written 0600, owned by the container's user, and rewritten every launch — this is the platform's copy of a value the platform holds, so a rotation cannot leave a stale one behind.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
{ "image": "", "command": [ "" ], "args": [ "" ], "env": { "key": "" }, "secrets": [ { "secret": "projects//locations//secrets/", "environment_variable": "", "file_path": "" } ], "port": 0, "protocol": "PROTOCOL_HTTP1", "resources": { "cpu": { "vcpus": 0, "reserved_millis": "0", "limit_millis": "0", "architecture": "CPU_ARCHITECTURE_X86_64", "required_features": [ "" ], "dedicated_cores": false, "isolate_smt_siblings": false }, "memory": { "size_bytes": "0", "reserved_bytes": "0", "balloon_enabled": false, "hugepages": false, "hotplug_max_bytes": "0", "shared": false }, "disks": [ { "disk_id": "my-disk", "source": { "image": "" }, "size_bytes": "0", "type": "DISK_TYPE_LOCAL_NVME", "read_only": false, "boot": false, "delete_on_instance_delete": false, "io_limits": { "bandwidth_bytes_per_second": "0", "bandwidth_burst_bytes": "0", "iops": "0", "iops_burst": "0" }, "direct_io": false } ], "network_interfaces": [ { "interface_id": "my-interface", "network": "", "private_ipv4": "", "public_address_mode": "PUBLIC_ADDRESS_MODE_NONE", "tier": "NETWORK_TIER_STANDARD", "rate_limits": { "ingress_bits_per_second": "0", "egress_bits_per_second": "0", "ingress_burst_bits": "0", "egress_burst_bits": "0", "ingress_packets_per_second": "0", "egress_packets_per_second": "0" }, "mac_address": "", "multi_queue": false, "tags": [ "" ], "deny_inbound": false, "no_default_route": false } ], "placement": { "zones": [ "" ], "required_node_selectors": [ { "key": "", "operator": "OPERATOR_IN", "values": [ "" ] } ], "preferred_node_selectors": [ { "key": "", "operator": "OPERATOR_IN", "values": [ "" ] } ], "placement_group": "", "policy": "PLACEMENT_POLICY_PACK", "capacity_class": "", "disallow_preemptible": false, "node": "" }, "scheduling": { "on_host_maintenance": "ON_HOST_MAINTENANCE_MIGRATE", "restart_policy": "RESTART_POLICY_NEVER", "preemptible": false, "preemption_notice": "300s", "max_run_duration": "300s", "termination_grace_period": "300s", "preemptible_fallback": false } }, "max_concurrency": 0, "request_timeout": "300s", "startup_timeout": "300s", "vpc": "projects//locations//vpcs/", "image_pull_secret": "projects//locations//secrets/"}ScalingSpec is how many instances a sandbox runs.
min_instancesint32Instances to keep running even with no traffic. Defaults to 0.
Zero is scale-to-zero. One or more is a container that runs permanently, and is charged for continuously.
max_instancesint32Ceiling. Defaults to 10.
A ceiling rather than a target, and the thing standing between a runaway
trigger and a bill. It is also what
sandbox.plany.cloud/max_instances is counted in, so it cannot be raised
past what the project's quota allows.
idle_timeoutDurationHow long an instance may sit with nothing in flight before it is released. Defaults to 15 minutes.
Measured from the last request finishing, as counted by Plany in front of the container — the container is never asked whether it is busy.
{ "min_instances": 0, "max_instances": 0, "idle_timeout": "300s"}SecretAttachment makes one credential available to a container. The same shape as a game server's secret attachment, field for field.
secretstringRequiredprojects/{project}/locations/{location}/secrets/{secret}, optionally with
/versions/{version}.
Unqualified resolves to the latest enabled version, so a rotation reaches this container on its next start with nothing to redeploy. Pin a version only when a revision must not follow a rotation.
environment_variablestringRequiredThe environment variable the container sees it as.
Upper case, digits and underscores, not starting with a digit. The payload must be valid UTF-8 to be injected this way; a binary key is refused at launch rather than silently mangled.
file_pathstringAlso write the value to this absolute path inside the container.
For the things that will not go in a variable: a service account JSON, a TLS key, a licence file something reads by path. Written 0600, owned by the container's user, and rewritten every launch — this is the platform's copy of a value the platform holds, so a rotation cannot leave a stale one behind.
{ "secret": "projects//locations//secrets/", "environment_variable": "", "file_path": ""}deliveriesrepeated FailedDeliverytriggerstringevent_typeEventTypeEVENT_TYPE_CONNECTION_OPENEDEVENT_TYPE_CONNECTION_CLOSEDEVENT_TYPE_PLAYER_JOINEDEVENT_TYPE_PLAYER_LEFTEVENT_TYPE_SERVER_READYEVENT_TYPE_SERVER_STOPPINGevent_idstringThe event's id, to quote when asking Plany support about it.
game_serverstringWhat the game server was, when the event named one.
reasonstringThe last failure, in words. An HTTP status, a timeout, a sandbox that could not be started.
attemptsint32event_timeTimestamplast_attempt_timeTimestampnext_page_tokenstringrevisionsrepeated SandboxRevisionNewest first.
namestringIdentifier.../sandboxes/{sandbox}/revisions/{revision}, where the last segment is the sandbox id and an ordinal — api-00007.
uidstringOutput onlyThe template exactly as it was when this revision was minted.
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
commandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
image_digeststringOutput onlyThe image, resolved: sha256:…. What every instance of this revision runs, whatever the tag points at now.
For an image in another registry deployed by tag, this is set while the revision deploys, when the tag is first resolved.
schematicstringOutput onlyprojects/{project}/locations/{location}/schematics/{schematic} — the
captured disk instances boot from.
STATE_PROVISIONINGSTATE_BAKINGSTATE_READYSTATE_FAILEDstate_reasonstringOutput onlycreated_bystringOutput onlyWho deployed it, as a principal name.
When the bake finished. Empty for a revision that never became READY.
next_page_tokenstringtriggersrepeated SandboxTriggerWhat to deliver.
EVENT_TYPE_CONNECTION_OPENEDEVENT_TYPE_CONNECTION_CLOSEDEVENT_TYPE_PLAYER_JOINEDEVENT_TYPE_PLAYER_LEFTEVENT_TYPE_SERVER_READYEVENT_TYPE_SERVER_STOPPINGnamestringIdentifieruidstringOutput onlydisplay_namestringgame_serverstringDeliver only events about this game server.
projects/{project}/locations/{location}/gameServers/{game_server}, or
empty for every game server in the project. The narrow case is the common
one — a trigger for a specific fleet — and the wide one is what a project's
analytics wants.
pathstringThe request path events are delivered to. Defaults to /.
So one sandbox can serve several triggers and route on the path, which is how an ordinary web framework already wants to be written.
stateSandboxTrigger.StateSTATE_ACTIVESTATE_PAUSEDfailed_delivery_countint64Output onlyHow many deliveries have been abandoned after exhausting their retries.
Output, and the number worth putting on a console page: a trigger that stopped working is otherwise indistinguishable from one nothing has happened for.
etagstringOutput onlynext_page_tokenstringsandboxesrepeated SandboxWhat to deploy.
Writing this mints a revision. Reading it gives back what was last written,
which is not necessarily what is serving: a revision that failed to bake
leaves serving_revision on the one before it, and the difference between
the two fields is exactly "a deploy that has not landed".
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
commandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
namestringIdentifierprojects/{project}/locations/{location}/sandboxes/{sandbox}.
Regional. The image is in one region's object store, the instances are on one region's hosts, and the URL names the region — a customer wanting two regions has a sandbox in each, which is the honest arrangement rather than a global name whose availability quietly depends on a region they never chose.
uidstringOutput onlyServer-assigned, immutable, unique for the life of the platform.
display_namestringWhat a person calls it. Free text.
descriptionstringlabelsmap<string, string>scalingScalingSpecmin_instancesint32Instances to keep running even with no traffic. Defaults to 0.
Zero is scale-to-zero. One or more is a container that runs permanently, and is charged for continuously.
max_instancesint32Ceiling. Defaults to 10.
A ceiling rather than a target, and the thing standing between a runaway
trigger and a bill. It is also what
sandbox.plany.cloud/max_instances is counted in, so it cannot be raised
past what the project's quota allows.
idle_timeoutDurationHow long an instance may sit with nothing in flight before it is released. Defaults to 15 minutes.
Measured from the last request finishing, as counted by Plany in front of the container — the container is never asked whether it is busy.
ingressIngressINGRESS_PUBLICINGRESS_INTERNALserving_revisionstringThe revision serving traffic, or empty to serve the newest READY one.
Setting it is a rollback, and it is a rollback rather than a redeploy on purpose: the bytes are already baked, so it takes effect in the time it takes to start an instance rather than the time it takes to pull an image that may no longer exist under the tag it was pushed with.
latest_revisionstringOutput onlyThe newest revision, whatever its state.
urlstringOutput onlyWhere requests are sent, e.g. https://api-7f3a91.eu-west-2.plany.run.
Stable for the life of the sandbox and unaffected by every deploy,
rollback and scale to zero, because that is the only property that makes it
safe to write into a client. Empty for INGRESS_INTERNAL.
The suffix is not derived from the sandbox id alone: a name a stranger can guess from a project's naming convention is a public endpoint nobody published.
service_accountstringaccounts/{account}/serviceAccounts/{service_account}.
The identity the container runs as, and what it pulls its image and reads its secrets with. Left empty, Plany provisions one, binds it narrowly, and revokes it on delete — the derived-name arrangement Playpute uses, which is what lets a teardown tell an identity the platform made from one a customer attached.
Set it to attach an identity of your own, which requires
iam.serviceAccounts.actAs on it. Plany grants a customer-supplied
identity nothing: whatever it can reach, it could already reach.
STATE_PROVISIONINGSTATE_READYSTATE_FAILEDSTATE_DELETINGstate_reasonstringOutput onlyWhy the sandbox is in this state, for a person to read.
Carries a failed revision's reason verbatim — an image that could not be resolved, a container that exited during the bake, a secret the identity could not read. Never a credential and never a presigned URL: this is rendered in the console and returned to any caller who can read the sandbox.
instance_countint32Output onlyHow many instances are running right now.
Zero is an ordinary answer for a healthy sandbox. has_instance_count
rides beside it because "no instances" and "the ingress did not report"
are different facts and only one of them is normal.
has_instance_countboolOutput onlyannotationsmap<string, string>generationint64Output onlyBumped by every change to the desired state.
observed_generationint64Output onlyThe generation that has been acted on. When it is behind generation, a change is rolling out.
etagstringOutput onlynext_page_tokenstringSandbox is a container service: a name, a URL, and whatever is currently deployed to it.
What to deploy.
Writing this mints a revision. Reading it gives back what was last written,
which is not necessarily what is serving: a revision that failed to bake
leaves serving_revision on the one before it, and the difference between
the two fields is exactly "a deploy that has not landed".
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
disksrepeated DiskSpecBlock devices, in attachment order. Exactly one must set boot unless the
workload boots from an initramfs alone.
network_interfacesrepeated NetworkInterfaceSpecplacementPlacementSpecschedulingSchedulingSpeccommandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
secretstringRequiredprojects/{project}/locations/{location}/secrets/{secret}, optionally with
/versions/{version}.
Unqualified resolves to the latest enabled version, so a rotation reaches this container on its next start with nothing to redeploy. Pin a version only when a revision must not follow a rotation.
environment_variablestringRequiredThe environment variable the container sees it as.
Upper case, digits and underscores, not starting with a digit. The payload must be valid UTF-8 to be injected this way; a binary key is refused at launch rather than silently mangled.
file_pathstringAlso write the value to this absolute path inside the container.
For the things that will not go in a variable: a service account JSON, a TLS key, a licence file something reads by path. Written 0600, owned by the container's user, and rewritten every launch — this is the platform's copy of a value the platform holds, so a rotation cannot leave a stale one behind.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
namestringIdentifierprojects/{project}/locations/{location}/sandboxes/{sandbox}.
Regional. The image is in one region's object store, the instances are on one region's hosts, and the URL names the region — a customer wanting two regions has a sandbox in each, which is the honest arrangement rather than a global name whose availability quietly depends on a region they never chose.
uidstringOutput onlyServer-assigned, immutable, unique for the life of the platform.
display_namestringWhat a person calls it. Free text.
descriptionstringlabelsmap<string, string>scalingScalingSpecmin_instancesint32Instances to keep running even with no traffic. Defaults to 0.
Zero is scale-to-zero. One or more is a container that runs permanently, and is charged for continuously.
max_instancesint32Ceiling. Defaults to 10.
A ceiling rather than a target, and the thing standing between a runaway
trigger and a bill. It is also what
sandbox.plany.cloud/max_instances is counted in, so it cannot be raised
past what the project's quota allows.
idle_timeoutDurationHow long an instance may sit with nothing in flight before it is released. Defaults to 15 minutes.
Measured from the last request finishing, as counted by Plany in front of the container — the container is never asked whether it is busy.
ingressIngressINGRESS_PUBLICINGRESS_INTERNALserving_revisionstringThe revision serving traffic, or empty to serve the newest READY one.
Setting it is a rollback, and it is a rollback rather than a redeploy on purpose: the bytes are already baked, so it takes effect in the time it takes to start an instance rather than the time it takes to pull an image that may no longer exist under the tag it was pushed with.
latest_revisionstringOutput onlyThe newest revision, whatever its state.
urlstringOutput onlyWhere requests are sent, e.g. https://api-7f3a91.eu-west-2.plany.run.
Stable for the life of the sandbox and unaffected by every deploy,
rollback and scale to zero, because that is the only property that makes it
safe to write into a client. Empty for INGRESS_INTERNAL.
The suffix is not derived from the sandbox id alone: a name a stranger can guess from a project's naming convention is a public endpoint nobody published.
service_accountstringaccounts/{account}/serviceAccounts/{service_account}.
The identity the container runs as, and what it pulls its image and reads its secrets with. Left empty, Plany provisions one, binds it narrowly, and revokes it on delete — the derived-name arrangement Playpute uses, which is what lets a teardown tell an identity the platform made from one a customer attached.
Set it to attach an identity of your own, which requires
iam.serviceAccounts.actAs on it. Plany grants a customer-supplied
identity nothing: whatever it can reach, it could already reach.
STATE_PROVISIONINGSTATE_READYSTATE_FAILEDSTATE_DELETINGstate_reasonstringOutput onlyWhy the sandbox is in this state, for a person to read.
Carries a failed revision's reason verbatim — an image that could not be resolved, a container that exited during the bake, a secret the identity could not read. Never a credential and never a presigned URL: this is rendered in the console and returned to any caller who can read the sandbox.
instance_countint32Output onlyHow many instances are running right now.
Zero is an ordinary answer for a healthy sandbox. has_instance_count
rides beside it because "no instances" and "the ingress did not report"
are different facts and only one of them is normal.
has_instance_countboolOutput onlyannotationsmap<string, string>generationint64Output onlyBumped by every change to the desired state.
observed_generationint64Output onlyThe generation that has been acted on. When it is behind generation, a change is rolling out.
etagstringOutput onlySandboxRevision is one immutable deployment.
Created by writing Sandbox.template, never directly. Nothing about it can
be edited: a revision that could change would make serving_revision a
rollback to something other than what was there, which is worse than no
rollback at all.
namestringIdentifier.../sandboxes/{sandbox}/revisions/{revision}, where the last segment is the sandbox id and an ordinal — api-00007.
uidstringOutput onlyThe template exactly as it was when this revision was minted.
imagestringRequiredEither a repository in Plany's container registry —
projects/{project}/locations/{location}/repositories/{repository}
followed by :{tag} or @{digest} — or an image in an external registry,
written as docker pull would take it: ghcr.io/acme/api:1.4,
docker.io/library/nginx@sha256:….
A Plany repository must be in this project and region — the registry is
regional and a pull crossing a region would be bytes leaving the failure
domain the sandbox is in. An external image is pulled anonymously, or with
image_pull_secret when the registry needs a credential.
A tag is resolved to a digest when the revision is minted, and the digest
is what is stored and pulled. So a tag that moves does not change a running
revision, which is the property that makes serving_revision a rollback
rather than a hope.
CPU, memory and disk, in the same terms as a game server's.
Note CpuSpec.reserved_millis against vcpus: the gap between them is burst headroom, and it is what makes a sandbox that is idle most of the time cheap without making it slow when it is not.
disksrepeated DiskSpecBlock devices, in attachment order. Exactly one must set boot unless the
workload boots from an initramfs alone.
network_interfacesrepeated NetworkInterfaceSpecplacementPlacementSpecschedulingSchedulingSpeccommandrepeated stringOverride the image's ENTRYPOINT. Empty uses the image's own.
An argument list, never a shell string: no shell is run for you.
argsrepeated stringOverride the image's CMD. Empty uses the image's own.
envmap<string, string>Environment variables, merged over the image's own.
For values that are not sensitive. A credential goes in secrets, where it
is fetched by the container's own identity at launch and never sits in this
message, in this service's database, in an audit entry, or on a console
page.
secretsrepeated SecretAttachmentCredentials to make available, fetched when an instance starts by the sandbox's own identity.
Attaching allows this sandbox to read that one secret and stops the secret being deleted while it is attached. The value is never stored in the sandbox's configuration.
secretstringRequiredprojects/{project}/locations/{location}/secrets/{secret}, optionally with
/versions/{version}.
Unqualified resolves to the latest enabled version, so a rotation reaches this container on its next start with nothing to redeploy. Pin a version only when a revision must not follow a rotation.
environment_variablestringRequiredThe environment variable the container sees it as.
Upper case, digits and underscores, not starting with a digit. The payload must be valid UTF-8 to be injected this way; a binary key is refused at launch rather than silently mangled.
file_pathstringAlso write the value to this absolute path inside the container.
For the things that will not go in a variable: a service account JSON, a TLS key, a licence file something reads by path. Written 0600, owned by the container's user, and rewritten every launch — this is the platform's copy of a value the platform holds, so a rotation cannot leave a stale one behind.
portint32What the container listens on. Defaults to 8080.
One port. A sandbox is reached through one URL, and a second listener would be a second address to name, route, certificate and bill for — which is a product decision rather than a field.
protocolProtocolHow the container is spoken to.
The ingress terminates TLS either way and speaks this to the container, so a gRPC sandbox gets prior-knowledge h2c and an HTTP one does not — a distinction that cannot be sniffed reliably and must not be guessed.
PROTOCOL_HTTP1PROTOCOL_H2Cmax_concurrencyint32How many requests one instance handles at once. Defaults to 80.
The number the autoscaler divides by, and the one customers get wrong in both directions. Set it to 1 for a container that is not concurrency-safe; leave it high for anything that spends its time waiting on something else.
request_timeoutDurationHow long a request may take before the ingress gives up. Defaults to 5 minutes, capped at 60.
Measured from the moment the request is accepted, which includes any wait for a scaled-to-zero instance to start. That is deliberate: it is the number the caller experiences, and a timeout that excluded the cold start would be a promise about the half the platform controls.
startup_timeoutDurationHow long to wait for the container to accept its first connection before calling the start failed. Defaults to 4 minutes.
vpcstringA private network the container can reach, by its addresses, as well as the internet. Empty means none.
A sandbox is not on a customer's network: its instances run on a network Plany manages for Sandbox in the project, which nothing else is on. This connects them to one more. Each instance gets an address in the network with no default route through it — only traffic for the network's own range goes that way — and nothing on the network can open a connection to it, whatever that network's firewall rules say. So a sandbox can call a database or a game server's admin port, and cannot be called back.
On the template, so it is part of a revision: a deploy that connects a
network can be rolled back to one that did not. Requires vpc.vpcs.use
on the network, which must be in the sandbox's own project and region.
image_pull_secretstringThe credential an external registry needs, as a Secrets Manager secret: projects/{project}/locations/{location}/secrets/{secret}, optionally with /versions/{version}. Empty pulls anonymously. Refused for an image in Plany's own registry, which is pulled by the sandbox's identity.
The secret's payload is JSON, {"username": "…", "password": "…"} — the pair docker login takes, where the password is usually an access token.
Used only to pull the image when a revision is deployed. Sandbox grants the sandbox's identity roles/secretmanager.accessor on this one secret; it is never put in the container's environment.
image_digeststringOutput onlyThe image, resolved: sha256:…. What every instance of this revision runs, whatever the tag points at now.
For an image in another registry deployed by tag, this is set while the revision deploys, when the tag is first resolved.
schematicstringOutput onlyprojects/{project}/locations/{location}/schematics/{schematic} — the
captured disk instances boot from.
STATE_PROVISIONINGSTATE_BAKINGSTATE_READYSTATE_FAILEDstate_reasonstringOutput onlycreated_bystringOutput onlyWho deployed it, as a principal name.
When the bake finished. Empty for a revision that never became READY.
SandboxTrigger runs a sandbox when something happens on the platform.
The half of this product a generic cloud cannot offer. Plany already knows when a player joined, when a server went quiet and when a connection arrived at something that was not running, because it learned all three from the game server's own output and from the socket its connection proxy accepted. A trigger is a customer's code attached to one of those facts.
Delivery is a request to the sandbox, on the same path an HTTP caller takes: the same wake, the same concurrency accounting, the same meter. A second way in would be a second place for each of those to be wrong.
What to deliver.
EVENT_TYPE_CONNECTION_OPENEDEVENT_TYPE_CONNECTION_CLOSEDEVENT_TYPE_PLAYER_JOINEDEVENT_TYPE_PLAYER_LEFTEVENT_TYPE_SERVER_READYEVENT_TYPE_SERVER_STOPPINGnamestringIdentifieruidstringOutput onlydisplay_namestringgame_serverstringDeliver only events about this game server.
projects/{project}/locations/{location}/gameServers/{game_server}, or
empty for every game server in the project. The narrow case is the common
one — a trigger for a specific fleet — and the wide one is what a project's
analytics wants.
pathstringThe request path events are delivered to. Defaults to /.
So one sandbox can serve several triggers and route on the path, which is how an ordinary web framework already wants to be written.
stateSandboxTrigger.StateSTATE_ACTIVESTATE_PAUSEDfailed_delivery_countint64Output onlyHow many deliveries have been abandoned after exhausting their retries.
Output, and the number worth putting on a console page: a trigger that stopped working is otherwise indistinguishable from one nothing has happened for.
etagstringOutput onlyEventType is something that happens on the platform which a sandbox can be started by: a connection, a player joining or leaving, a server becoming ready or stopping.
| Value | Description |
|---|---|
| EVENT_TYPE_UNSPECIFIED | |
| EVENT_TYPE_CONNECTION_OPENED | Somebody connected to a game server. The earliest signal the platform has, and the only one that exists for a server that is not running: the connection proxy accepts the socket before anything has started. `woke_server` on the delivered event says whether this connection is what started it. |
| EVENT_TYPE_CONNECTION_CLOSED | A connection ended. |
| EVENT_TYPE_PLAYER_JOINED | A player joined, identified by name. Later than the connection — it comes from the server's own output, so the server is by definition running — and richer, because it carries who. The delivered event carries `active_player_count` after applying it. |
| EVENT_TYPE_PLAYER_LEFT | A player left. Note what this does not promise. Three of five end reasons mean nobody announced a departure and the platform inferred one, and `END_REASON_EXPIRED` in particular is a ceiling rather than a measurement. The reason rides on the delivered event so a customer's code can tell them apart. |
| EVENT_TYPE_SERVER_READY | A game server finished starting and is accepting players. |
| EVENT_TYPE_SERVER_STOPPING | A game server is shutting down. |
Ingress says who can reach a sandbox.
| Value | Description |
|---|---|
| INGRESS_UNSPECIFIED | Public, which is what a sandbox with an HTTP surface is. |
| INGRESS_PUBLIC | Reachable at `url` from the internet, subject to whatever the container itself checks. Deliberately not authenticated by the platform. A sandbox is very often a webhook receiver or a game client's backend, and neither can present a Plany credential. `sandbox.sandboxes.invoke` exists for the other case and is enforced only on `INGRESS_INTERNAL`. |
| INGRESS_INTERNAL | No public address. Reachable by triggers, and by a caller holding `sandbox.sandboxes.invoke` on this sandbox. The right answer for a sandbox that only ever reacts to platform events: it has no URL to leak, no unauthenticated surface, and no reason to have one. |
Protocol is what the ingress speaks to a container.
| Value | Description |
|---|---|
| PROTOCOL_UNSPECIFIED | HTTP/1.1. |
| PROTOCOL_HTTP1 | |
| PROTOCOL_H2C | HTTP/2 with no TLS, which is what a gRPC server expects. Requests reach your sandbox's URL over TLS either way. |
| Value | Description |
|---|---|
| STATE_UNSPECIFIED | |
| STATE_PROVISIONING | A revision is being baked and nothing serves yet. The first deploy only. A *later* deploy leaves the sandbox READY on its previous revision throughout, which is the difference between a bad push costing a customer their next release and costing them their service. |
| STATE_READY | Serving. Instances may still be zero — a sandbox at rest is ready, and rendering it as anything else would teach customers that the normal state of a scale-to-zero service looks broken. |
| STATE_FAILED | No revision can serve. `state_reason` says which one failed and why. |
| STATE_DELETING | The row is gone from every list; instances and identity are being torn down. |
| Value | Description |
|---|---|
| STATE_UNSPECIFIED | |
| STATE_PROVISIONING | Accepted; the image is being resolved. |
| STATE_BAKING | The image is being pulled and prepared. Once per revision. Every instance afterwards starts from the prepared copy, which is why a cold start is seconds rather than an image pull. |
| STATE_READY | Startable. |
| STATE_FAILED | `state_reason` says why. A failed revision leaves the previous one serving. |
| Value | Description |
|---|---|
| STATE_UNSPECIFIED | |
| STATE_ACTIVE | |
| STATE_PAUSED | Not delivering. A customer's own pause, and the thing to reach for when a sandbox is failing every delivery rather than deleting the trigger and losing its configuration. |