Docs

Remote Commands

Your software runs in the customer's cloud. Commands are how your backend talks to it — invoke explicit handlers on remote deployments without opening inbound ports.

This is the primary way your cloud service communicates with customer deployments. An AI agent sends tool calls to a worker in the customer's VPC. A data connector runs queries against a private database. A dashboard pulls live status. Each of these is a command: your code defines handlers, your backend invokes them, and responses come back. No open ports, no VPN, no generic shell.

Define a Handler

Every command is addressed to one command-enabled resource: a Worker, Container, or Daemon. How the target runs its handler depends on that compute type.

On a Worker

A Worker receives pushed commands through Alien's Worker runtime; it never polls the command server. Register handlers with command from @alienplatform/sdk. Pass a schema to validate and type the input — any Standard Schema validator works (zod, valibot, arktype):

import { command } from "@alienplatform/sdk"
import { z } from "zod"

command(
  "generate-report",
  z.object({ startDate: z.string(), endDate: z.string() }),
  async ({ startDate, endDate }) => {
    const data = await fetchData(startDate, endDate)
    return { report: aggregate(data), rowCount: data.length }
  },
)

command("run-migration", async params => {
  // No schema: params is the decoded JSON, typed as unknown.
  await migrate(params)
  return { status: "completed" }
})

The handler's optional second argument carries per-invocation metadata — commandId, attempt, and deadline. Delivery is at-least-once, so use attempt to make redeliveries idempotent:

command("run-migration", async ({ version }: { version: string }, { attempt, commandId }) => {
  if (attempt > 1 && (await alreadyApplied(version))) {
    return { status: "completed", deduped: commandId }
  }
  await migrate(version)
  return { status: "completed" }
})

Enable commands on the Worker:

alien.ts
const toolExecutor = new alien.Worker("tool-executor")
  .code({ type: "source", src: "./", toolchain: { type: "typescript" } })
  .commandsEnabled(true)
  .permissions("execution")
  .build()

The handler receives the invocation's parameters directly and returns the response value.

On a Container or Daemon

A Container or Daemon has no Worker runtime in front of it. Enabling commands injects configuration only; the application must run an explicit pull receiver from @alienplatform/commands (or alien-commands in Rust). That receiver leases commands addressed to this resource, dispatches them to your handlers, and submits the responses — all over outbound HTTPS. Alien does not start a receiver sidecar, and the Operator does not run the app's receiver or handlers.

The receiver is an ordinary library. @alienplatform/commands is pure fetch over the command wire protocol, with no native addon, no gRPC, and no injected globals, so it runs in a plain Node 18+ or Bun process. Nothing about the process has to be Alien-aware beyond starting the receiver and reading the environment below.

import { createCommandReceiver } from "@alienplatform/commands"
import { z } from "zod"

const receiver = createCommandReceiver()

receiver.command(
  "generate-report",
  z.object({ startDate: z.string(), endDate: z.string() }),
  async ({ startDate, endDate }) => {
    const data = await fetchData(startDate, endDate)
    return { report: aggregate(data), rowCount: data.length }
  },
)

async function main() {
  await receiver.run()
}

void main()

createCommandReceiver() (TypeScript) and Receiver::from_env() (Rust) read the same environment, which Alien injects for a command-enabled resource. Both receivers accept the identical variable names.

VariableRequiredValue
ALIEN_COMMANDS_URLYesBase URL of the command server. Must be an http or https URL.
ALIEN_COMMANDS_TOKENOne of the twoBearer token for the receiver's outbound lease and response requests.
ALIEN_COMMANDS_TOKEN_FILEOne of the twoPath to a file holding that token. Reread once after a 401, so projected credentials can rotate without restarting the app.
ALIEN_DEPLOYMENT_IDYesDeployment the leased commands belong to.
ALIEN_COMMANDS_TARGET_RESOURCE_IDYesThis resource's id inside the deployment's stack.
ALIEN_COMMANDS_TARGET_RESOURCE_TYPEYescontainer or daemon, lowercase.

Validation runs synchronously at construction and fails fast. A missing, empty, or invalid value throws COMMAND_RECEIVER_CONFIG_INVALID naming the offending variable, so a misconfigured process dies at startup instead of polling nothing.

ALIEN_COMMANDS_TARGET_RESOURCE_TYPE accepts only container and daemon. worker is rejected, along with any other value. A receiver must not guess its own target type, and a Worker is pushed to rather than leased.

The lease loop is tunable through ALIEN_COMMANDS_POLL_INTERVAL_MS (default 5000), ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS (30000), ALIEN_COMMANDS_POLL_JITTER (0.1), ALIEN_COMMANDS_LEASE_SECONDS (60), ALIEN_COMMANDS_MAX_LEASES (1), and ALIEN_COMMANDS_DRAIN_TIMEOUT_MS (30000). The matching createCommandReceiver() options override the environment.

receiver.command(name, handler) decodes the JSON parameters and passes them as the handler's first argument; add a Standard Schema validator as the middle argument to validate and type them. For non-JSON payloads, receiver.handleRaw(name, handler) (Rust: receiver.handle_raw) hands you the raw parameter bytes as ctx.input, a Uint8Array you decode yourself. The handler's second argument is the context: an abort signal; the effective execution-budget deadline; commandId; attempt; the receiver's target (resourceId and resourceType); and optional W3C traceContext (traceparent and tracestate). Rust exposes the equivalent fields as cancellation, deadline, command_id, attempt, target, and trace_context, with the raw bytes reachable through ctx.input (and ctx.input_json() for typed decoding).

Whatever the handler returns is JSON-encoded and submitted as the command's success response.

run() drives the lease loop until you trigger graceful shutdown — receiver.stop() in TypeScript, or in Rust grab a handle with receiver.shutdown_handle() before moving the receiver into run(), then call handle.shutdown(). Both stop taking new leases and give in-flight handlers up to 30 seconds to finish by default; after that, remaining handlers are cancelled and their leases are released before run() resolves. Call run() alongside whatever else the process does (for example, serving an HTTP API).

Enable commands in the stack the same way:

alien.ts
const connector = new alien.Daemon("connector")
  .code({ type: "image", image: "ghcr.io/acme/connector:v1" })
  .commandsEnabled(true)
  .permissions("execution")
  .build()

Invoke from the CLI

The CLI has no --target flag: it relies on single-target inference, so it works when the deployment has exactly one command-capable resource. In a deployment where the connector Daemon is the only command-enabled resource:

alien commands invoke \
  --deployment acme-corp \
  --command generate-report \
  --params '{"startDate": "2025-01-01"}'

With more than one command-capable resource (say the tool-executor Worker and the connector Daemon), an untargeted invoke is rejected with COMMAND_TARGET_AMBIGUOUS — target explicitly from code with .target(name), below.

Invoke an operations plugin from the CLI

Operations plugins are separate from application-owned command receivers. Use alien operations invoke to run an enabled operation directly without an AI agent:

alien operations invoke \
  --project production \
  --deployment acme/prod \
  --operation kubernetes/rollout-status \
  --params '{"namespace": "app", "workload": "deployment/api"}'

Alien applies the project's operations approval policy before creating the command. For an auto-approved operation, the CLI waits for completion and prints the decoded JSON result. For an operation that requires approval, it reports that approval is required and does not dispatch a command.

Invoke from Code

Senders use CommandsClient from @alienplatform/commands. For an Alien-hosted deployment, create the client from the deployment ID and a server-side Alien API key:

import { CommandsClient } from "@alienplatform/commands"

const commands = await CommandsClient.forDeployment({
  deploymentId: "deployment_123",
  apiKey: process.env.ALIEN_API_KEY!,
})

const result = await commands
  .target("tool-executor") // the resource that hosts the handler
  .invoke("generate-report", {
    startDate: "2025-01-01",
    endDate: "2025-03-31",
  })

forDeployment() resolves the manager for the deployment and mints short-lived, command-only access. Reuse the client; it refreshes that connection before expiry and once after a manager 401.

For a self-hosted or directly addressed manager, construct the client with an explicit manager URL and token:

const commands = new CommandsClient({
  managerUrl: "https://manager.example.com",
  deploymentId: "deployment_123",
  token: process.env.ALIEN_COMMANDS_TOKEN,
})

Both clients use HTTP and go through the control plane. The command is stored there until Alien delivers it to the Worker runtime or the targeted Container or Daemon leases it over outbound HTTPS.

How It Works

  1. You invoke a command for a deployment. Code names the target with .target(resourceId); the CLI uses the deployment's sole command-capable resource and rejects ambiguous deployments.
  2. Alien stores the request on the command server.
  3. Alien resolves how commands reach that specific target:
    • A Worker always receives a push. In a push deployment, the Manager dispatches it through the platform's Worker delivery path. In a pull deployment, the Operator leases pending Worker work and relays it to the targeted Worker runtime; the Worker itself never polls.
    • A Container or Daemon always uses the app-owned pull receiver, regardless of the deployment model.
  4. The handler runs and produces a response.
  5. You read the result.

The customer's environment never needs to allow inbound traffic from Alien's control plane. Cloud-provider delivery or the in-environment Operator reaches the Worker runtime. App-owned Container/Daemon receivers lease only their own targeted work over outbound HTTPS.

Pull-receiver delivery is at-least-once — a lease that expires before its response is submitted is redelivered. Receiver handlers should tolerate running more than once for the same command (attempt in the receiver context tells you the delivery attempt).

Each handler runs under an execution budget of min(command deadline, lease expiry − a 5s safety margin): signal fires when the budget expires and the handler result is discarded, so an expired lease never has an in-flight duplicate submitting late. There is no lease renewal — long work should be sized to the deadline. When a handler fails, the sender sees a typed error code: HANDLER_ERROR (the handler threw), HANDLER_TIMEOUT (budget expired), or UNKNOWN_COMMAND (no handler registered for the name).

Delivery Mode Is Not the Deployment Model

"Push" and "pull" name two unrelated things in these docs. Command delivery is one axis. The deployment model is another.

Command deliveryDeployment model
Decided byThe target resource's typeThe customer's environment, at onboarding
Push meansAlien delivers the command to a Worker runtime, which never pollsThe Deployment Manager impersonates an identity in the customer's cloud and calls cloud APIs
Pull meansThe application leases its own commands with createCommandReceiver or alien_commands::ReceiverAn Operator inside the customer's environment polls outbound for releases
ScopeOne command at a timeThe whole deployment

The two axes do not constrain each other. A Worker is pushed to on a push deployment and on a pull deployment. A Container or Daemon leases its own commands on both. Moving a customer from push to pull changes who applies releases; it does not change how any handler receives a command, and it does not put the Operator in the path of a Container or Daemon receiver.

Real-World Examples

AI Agent — remote tool calls

import { command, storage } from "@alienplatform/sdk"

const workspace = storage("workspace")

command("read-file", async ({ path }: { path: string }) => {
  const object = await workspace.get(path)
  return { content: object.data.toString("utf8") }
})

command("write-file", async ({ path, content }: { path: string; content: string }) => {
  await workspace.put(path, Buffer.from(content))
  return { ok: true }
})

command("list-files", async ({ directory }: { directory: string }) => {
  return (await workspace.list(directory)).map((entry) => entry.location)
})

The agent harness runs in your cloud — planning, model calls, orchestration. When it needs to act, it sends commands to the Worker, Container, or Daemon in the customer's environment. The handler controls what leaves as the command response: a file, an aggregate, a diff, or a redacted excerpt.

Data Connector — query behind the firewall

import { command, vault } from "@alienplatform/sdk"
import { Pool, type PoolConfig } from "pg"

command("get-users", async ({ status, limit }: { status: string; limit: number }) => {
  const creds = vault("credentials")
  const config = await creds.getJson<PoolConfig>("warehouse")
  const pool = new Pool(config)
  const { rows } = await pool.query(
    "SELECT id, name, email FROM users WHERE status = $1 LIMIT $2",
    [status, limit ?? 100]
  )

  return { rows, count: rows.length }
})

Warehouse credentials stay in the customer's vault. The query result is the command response, so the handler should return only the rows, aggregates, or diagnostics you intend to expose.

On this page