Commands
A command runs a function in a customer's environment when your backend asks, and returns the result. It needs zero inbound networking, so the customer's security team opens no ports, no VPN, and no firewall rules.
Architecture diagram. acme-docs deployment (search (Container), index (KV)) connects outbound to the Alien manager (Commands, Results), via https 443, outbound.
Polling over HTTPS
The deployment connects out to Alien over port 443, takes the commands waiting for it, runs them, and sends back the results. This works on every platform, for every Container and Daemon, including on AWS, Google Cloud, and Azure.
The handler runs in a Container, next to the customer's data:
import { createCommandReceiver } from "@alienplatform/commands"
import { kv } from "@alienplatform/sdk"
import { z } from "zod"
const index = kv("index")
const receiver = createCommandReceiver()
const SearchInput = z.object({
prefix: z.string(),
limit: z.number().max(100),
})
receiver.command("search", SearchInput, async ({ prefix, limit }) => {
const page = await index.scan(`doc:${prefix}`, limit)
const keys = page.items.map(item => item.key)
return { results: keys }
})
await receiver.run()Your backend calls it by name and waits for the result:
import { CommandsClient } from "@alienplatform/commands"
const commands = await CommandsClient.forDeployment({
deploymentId: customer.deploymentId,
apiKey: process.env.ALIEN_API_KEY!,
})
const input = { prefix: "contracts/", limit: 20 }
const { results } = await commands.invoke<{ results: string[] }>("search", input)Your backend often needs an answer that only the customer's environment can give: a search over documents that can't leave their account, a query against a private database, a check on a host. Only what the handler returns leaves the environment: here, document keys, not document contents.
Commands are one of several ways your backend reaches a deployment; see Backend connections to compare them.
Push to Workers
Workers, Alien's serverless compute, can also receive commands pushed through the cloud's API. On AWS a Worker runs on Lambda, on Google Cloud on Cloud Run, and on Azure on Container Apps.
Architecture diagram. acme-docs deployment (processor (Worker), index (KV)) is reached inbound from the Alien manager (Commands, Results), via push, cloud API.
The manager calls the Worker through Lambda, Pub/Sub, or Service Bus, with the management identity that initial setup created. Initial setup is the one-time step the customer's admin runs; see Frozen and live.
A Worker registers handlers with command(). No receiver loop is needed:
import { command, kv } from "@alienplatform/sdk"
import { z } from "zod"
const index = kv("index")
const SearchInput = z.object({
prefix: z.string(),
limit: z.number().max(100),
})
command("search", SearchInput, async ({ prefix, limit }) => {
const page = await index.scan(`doc:${prefix}`, limit)
const keys = page.items.map(item => item.key)
return { results: keys }
})Your backend sends to a Worker exactly as it sends to a Container. On Kubernetes, a Worker's commands arrive by polling instead: the Operator, Alien's agent in the cluster, takes them over outbound HTTPS and calls the Worker inside the cluster. See Rust SDK for the full Worker setup.
Enable commands
Turn on commands for each resource that handles them in alien.ts:
const search = new alien.Container("search")
.code({ type: "image", image: "ghcr.io/acme/search:1.0.0" })
.link(index)
.commandsEnabled(true)
.permissions("search")
.build()Worker and Daemon take the same .commandsEnabled(true).
- The schema validates and types the parameters. Any Standard Schema library works, such as zod or valibot. In Rust, the input type implements
Deserialize. - The return value goes back as JSON.
- Python workloads can't receive commands.
Delivery is at least once. The handler's context carries commandId, attempt, and the optional deadline, so a handler that changes data can detect a redelivery:
const ReindexInput = z.object({ key: z.string() })
command("reindex", ReindexInput, async ({ key }, { attempt }) => {
// ...rebuild the index entry for key; safe to run twice
return { key, attempt }
})Send from your backend
CommandsClient from @alienplatform/commands needs the deployment ID and a server-side Alien API key, as in the example at the top of this page.
Create the key with alien api-keys create --for deployments, or under Settings → API Keys in the dashboard. Keep it on your server.
.target() names the resource that hosts the handler:
const { results } = await commands
.target("search")
.invoke<{ results: string[] }>("search", input)It's required when more than one resource in the stack has commands enabled. Without it, the invoke is rejected with COMMAND_TARGET_AMBIGUOUS.
Reuse the client. It refreshes its short-lived, command-only access before it expires, and once after a 401 from the manager.
Private manager
For a private manager, one you run in your own cloud, pass its URL and a token instead. Rust backends use the alien-commands-client crate this way:
const commands = new CommandsClient({
managerUrl: "https://manager.example.com",
deploymentId: customer.deploymentId,
token: process.env.ALIEN_COMMANDS_TOKEN!,
})
const { results } = await commands
.target("search")
.invoke<{ results: string[] }>("search", input)How delivery works
Every command takes the same path through the manager, the Alien server that tracks your deployments:
- Your backend creates the command with
POST /v1/commands. The manager stores it. - The manager delivers it to the target resource, by polling or by push.
- The handler runs in the deployment and posts its result back to the manager.
- Your backend's
invoke()checks the command's status until it has a result. It checks after 500 ms, then backs off up to every 5 seconds.
The target resource and the deployment model decide the delivery:
| Target | Delivery | Pickup time |
|---|---|---|
| Container or Daemon, any platform | A receiver in your process polls over outbound HTTPS: every 5 seconds, slowing to every 30 seconds while idle | Up to about 30 seconds when idle |
| Worker on Kubernetes, or any pull deployment | The Operator polls every 5 seconds over outbound HTTPS and calls the Worker inside the environment | Up to about 5 seconds |
| Worker in a push deployment (AWS, Google Cloud, Azure) | The manager invokes the Worker through Lambda, Pub/Sub, or Service Bus when the command is created | Immediate |
What you set up for each:
- Container or Daemon: run a receiver in your process; see Containers and Daemons. The workload needs outbound HTTPS to the manager.
- Worker: nothing beyond
command()handlers. The Operator is part of every pull deployment, and push uses the management identity.
Polling is how a command reaches an environment behind a firewall: the deployment opens the connection on port 443, so the network needs no inbound rule. Push needs no network path at all, because the manager calls the cloud provider's API, not the customer's network.
Your handler code is the same either way. Moving a customer from push to pull changes who delivers a Worker's commands, not how the handler receives them.
Payloads
Parameters and results are JSON. Up to 150 KB, they travel inside the command. Larger ones go through the manager's storage with signed URLs, without any change to your code.
Send IDs and small inputs, and return the smallest result your backend needs. For files, return a signed URL to the customer's own storage instead of the bytes.
Timeouts and failures
invoke() waits 60 seconds by default. Change it with timeoutMs on the client or on each call. When it runs out, invoke() throws COMMAND_TIMEOUT, but the command isn't canceled: it can still be delivered and run later.
To stop a command that hasn't finished in time, pass a deadline:
await commands.invoke("search", input, {
timeoutMs: 30_000,
deadline: new Date(Date.now() + 30_000),
idempotencyKey: `search-${requestId}`,
})- A command without a result by its deadline is marked expired and isn't delivered after that.
invoke()throwsCOMMAND_EXPIRED. idempotencyKeydeduplicates retried creates: the same key for the same target returns the existing command.- A Worker's handler runs within the Worker's timeout (
.timeoutSeconds(), default 180 seconds). - When the Operator delivers to a Worker, it holds the command for the Worker's timeout plus 60 seconds. If no result arrives by then, the command is delivered again.
A handler error reaches your backend as DEPLOYMENT_COMMAND_ERROR, with the handler's code:
| Error code | Cause |
|---|---|
UNKNOWN_COMMAND | No handler registered for the name |
HANDLER_ERROR | The handler threw. An AlienError thrown by the handler keeps its own code. |
HANDLER_TIMEOUT | A receiver handler's budget expired |
DELIVERY_FAILED | The Worker's runtime refused the push |
Send from the CLI
Use the CLI for testing and support:
alien commands invoke \
--deployment customer-1/production \
--command search \
--params '{"prefix": "contracts/", "limit": 20}'--deploymenttakes a deployment ID or<deployment-group>/<deployment-name>. A deployment group holds a customer's deployments, usually one group per customer.--targetnames the resource when more than one has commands enabled.--timeoutsets the wait in seconds (default60).
Against a local alien dev session, use alien dev commands invoke --deployment local-dev/default ....
The dashboard doesn't send commands. Operations → Commands lists the commands dispatched to the project's deployments.
Containers and Daemons
A Container or Daemon has no Worker runtime in front of it, so your process runs the receiver, as in the example at the top of this page. Alien doesn't start a sidecar, and the Operator doesn't relay a Container's commands. If your process doesn't run a receiver, its commands are never picked up.
createCommandReceiver()from@alienplatform/commandsuses plainfetchwith no native addon, so it runs in any Node 18+ or Bun process.- Rust services use
alien_commands::Receiver::from_env()from thealien-commandscrate, with thereceiverfeature. - Run it alongside whatever else the process does, such as serving HTTP.
The receiver reads environment variables that Alien injects when .commandsEnabled(true) is set:
| Variable | Value |
|---|---|
ALIEN_COMMANDS_URL | Base URL of the manager's command API (http or https) |
ALIEN_COMMANDS_TOKEN or ALIEN_COMMANDS_TOKEN_FILE | Bearer token, or a file holding it. The file is reread once after a 401, so the token can rotate without a restart. |
ALIEN_DEPLOYMENT_ID | The deployment the commands belong to |
ALIEN_COMMANDS_TARGET_RESOURCE_ID | This resource's ID in the stack |
ALIEN_COMMANDS_TARGET_RESOURCE_TYPE | container or daemon. worker is rejected. |
A missing or invalid value throws COMMAND_RECEIVER_CONFIG_INVALID naming the variable, so a misconfigured process fails at startup instead of polling nothing.
Lease loop
Each check asks the manager for up to ALIEN_COMMANDS_MAX_LEASES commands and holds each one for a lease. The matching createCommandReceiver() options override these variables:
| Variable | Default | Meaning |
|---|---|---|
ALIEN_COMMANDS_POLL_INTERVAL_MS | 5000 | Time between checks while commands arrive |
ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS | 30000 | Longest time between checks; an empty check doubles the wait up to this |
ALIEN_COMMANDS_POLL_JITTER | 0.1 | Random spread applied to each wait |
ALIEN_COMMANDS_LEASE_SECONDS | 60 | How long a leased command stays with this receiver |
ALIEN_COMMANDS_MAX_LEASES | 1 | Commands handled at once |
ALIEN_COMMANDS_DRAIN_TIMEOUT_MS | 30000 | Time in-flight handlers get after stop() |
A receiver handler runs under a budget of the command's deadline or the lease expiry minus 5 seconds, whichever comes first. When it runs out, signal fires (ctx.cancellation in Rust), the result is discarded, and the sender gets HANDLER_TIMEOUT.
Leases aren't renewed, so size long work to the lease. A lease that expires before a result is submitted is delivered again, which is why handlers must tolerate running twice.
The handler context has signal, deadline, commandId, attempt, target, and an optional W3C traceContext. For non-JSON parameters, receiver.handleRaw(name, handler) passes the raw bytes as ctx.input.
run() leases commands until you call receiver.stop(). In Rust, take receiver.shutdown_handle() before run() and call shutdown() on it. On stop, the receiver stops leasing and gives in-flight handlers the drain timeout before it cancels them and releases their leases.
Commands and operations
Commands have no approval step. For actions that need the customer's approval, use Remote Operator operations.
To run an operation, a built-in action such as restarting a workload, use alien operations invoke. alien commands invoke only reaches your own handlers: given an operation's name, it fails with COMMAND_IS_OPERATION and prints the command to run instead.
Next
- Remote bindings: read and write one resource in the customer's account, without a handler
- Backend connections: every way your backend and a deployment talk, compared
- Testing: send commands from tests