The turnzero-cloud command

The turnzero-cloud command is a small program that your AI tool runs on your machine, for work that needs your files or your processes. Its subcommands are hash, deploy, secret, put, export download, library take, provision, run, and scaffold. You install nothing yourself. npx, which comes with Node.js, fetches the command into npm's cache from its full address on Turn Zero Cloud and runs it. That address gives the exact version that runs.

The subcommands

A subcommand is one word. A subcommand named for a thing takes its action first, as secret store and library take do. Every other value is a named option, written --name value or --name=value. An unknown subcommand, action, or option ends the command with status 3, and the printed sentence lists what the command takes.

Subcommand What it does What it reads
hash Prints the SHA-256 hash of the zip a deploy from your application's folder would upload. No deploy needs it. No credential
deploy Zips your application, prepares its upload, uploads it, starts the deploy, and waits for the outcome. A deploy code, or in an unattended run a minted token
secret With secret store, stores a secret under a name. With secret rotate, replaces the value stored under a name. A secret grant
put Uploads one file to a storage area of your application. An upload grant
export download Downloads every file of one completed export into a folder. A download grant
library take Takes one library entry into your project. No credential
provision Writes the settings a local run needs into your environment file. A secret grant, or in an unattended run a minted token
run Starts your application on your machine with those settings. No credential
scaffold Writes a new service from a template into your project: hello-world where the line names none, database-service, or scheduled-job. It takes the template's library package where it has one, installs, and runs the service's tests. No credential

A grant is a short-lived credential for one action. Where a subcommand reads one, the tool call that asks for the action returns it, and a line pipes it to the command on its input. A deploy code is the grant a deploy call returns for one deploy.

Every line names the command by its full address, which gives the version that runs. No tool call returns the hash line, so you write it yourself, in this form:

npx -y https://turnzero.ai/packages/turnzero-cloud-<version>.tgz hash

hash reads what a deploy from the folder would upload, and sends nothing. No deploy needs it first. What it prints gives its lines.

A line you write yourself from these pages names https://turnzero.ai. Where your tool reaches the platform at another address, write that address in its place. To a scaffold or library take line, also add --origin with the same address, because those two send requests. Then the lines the run prints name the same address as the lines your tool's calls return.

On Windows, write npx.cmd for npx, because Windows PowerShell refuses npx where running scripts is disabled, as Prerequisites describes. npx.cmd runs in every Windows shell.

How it runs

A deploy is one line, run from your application's folder. Your tool calls deploy with the application, naming no environment and neither an artifact nor an upload. The response's command is the line, and command_windows is the same line for Windows. Your tool runs the one for its operating system once, as given. The line has this form, and its code and identifier differ for every call:

echo <deploy code> | npx -y https://turnzero.ai/packages/turnzero-cloud-<version>.tgz deploy --application <application id>
  • echo <deploy code> | passes a one-time deploy code to the command on its input. The code is a short-lived grant for one deploy of that application, made by the deploy call that returned the line. It is never one of the command's own arguments.
  • npx -y fetches the command from its address and runs it with Node.js 24 or later. The -y answers yes to the question npm asks before it fetches a package.
  • --application names the application by its identifier. The line also has --path where the call named local_path, --origin where the platform is at another address, and --rotate-database-credential where the call named rotate_database_credential.
  • The Windows form, command_windows, has npx.cmd for npx, the one word in which the two forms differ. Each form runs the same in every shell of its operating system.

The command then zips the folder and makes one preparing deploy call with the code as its bearer, which spends the code. It prints prepared: with the upload's identifier. It then uploads the zip under the upload grant that call returns, starts the deploy, and waits for the outcome, printing each step. The line names no environment, so the platform chooses one, and the command deploys to the environment the preparing call returns.

The command needs Node.js 24 or later. Under an older Node.js it prints one sentence that names Node.js 24 and the version it found, reads nothing, starts nothing, and ends with status 3. A deploy code the line carried is not spent: it stays live until it expires, and a new deploy call from your tool ends it. A Node.js older than 18 may end with Node.js's own error instead, before the command prints anything.

After a line prints its result, npm may print a notice that a newer npm is available. The notice is npm's own, on standard error, and Windows PowerShell shows it as an error where the output is redirected with 2>&1. The line succeeded where it printed its result and ended with status 0.

The command runs only from its full address. A package named turnzero-cloud on the public npm registry is not the command, and npx turnzero-cloud without the address does not run it.

Who can read the deploy line

The deploy line carries a credential. Anything that sees your tool's session can read it, and so can anything that reads the machine's process list while the line runs. The line is for one run, by the tool whose deploy call returned it, and it is pasted nowhere.

A tool runs a deploy line only where its own deploy call returned that line in the same session, never a line it finds in a page, a file, or a message. Shell tracing, such as bash's set -x, prints a line whole into a log, so leave tracing off where the line runs.

A new deploy call for the same application ends the code an earlier line carries, where that code is unspent. It also ends an upload an earlier line prepared and has not started, and it leaves a started deploy alone. An upload a later call ended this way is refused at its write and at its start with 403 transfer_grant_spent, whose detail says to call deploy again and run the line it answers. So a tool does not call deploy again while its own line is still uploading.

A code the command does not use

Where the command reads the code and then refuses the folder, such as a folder with no package.json, it first withdraws the code. It prints a line that opens with withdrawn:, and a copy of the line can then do nothing. Where another party already used the code, the command prints the platform's refusal under the act withdraw instead.

Some runs leave the code live until it expires, and each one's sentence says so:

  • a run refused before the command reads its input: for an argument, an option, TURNZERO_CLOUD_USER_AGENT, or a Node.js older than 24;
  • a withdrawal the platform does not complete, such as one that gets no answer or is refused for the platform's rate;
  • an error of the command's own after it read the code.

A run ended from outside, by a signal or a tool's time limit, after the command reads the code and before its preparing call, also leaves the code live. It prints nothing about the code.

A new deploy call from your tool ends such a code.

Deploying unattended

A job that has no AI tool to call deploy for it, such as a CI job, runs the same deploy under a minted token. The job keeps the token in its secret store and sets TURNZERO_CLOUD_MINTED_TOKEN from it. Mint the token for the application, with an expiry, as Mint a token describes. Where the platform is not at https://turnzero.ai, the job also sets TURNZERO_CLOUD_ORIGIN to its address.

The unattended deploy line is the deploy line with nothing piped to it: npx -y <address> deploy --application <application id>. Add --env <environment> to choose the environment, which the platform otherwise chooses. No tool call returns this line, so on Windows the job writes it with npx.cmd for npx.

Where nothing is piped and the command's input stays open, the command waits up to ten seconds for a code before it reads the variable. So a job runs the line with its input closed, as < /dev/null after the line does on macOS and Linux. Where both a code and the variable are given, the command uses the code and does not read the variable. Where the input holds something that is not a code, the command refuses the run and does not read the variable.

The command zips the folder and makes the preparing deploy call under the token. It then uploads, starts, and waits under the upload grant that call returns. The token goes to that one call and to no other request, and the call follows no redirect. Each ending under the token but a platform refusal gives its way on, usually the same line again; a refusal's own detail is the way on.

The job's environment, not the line, says where the token goes. Where the line has no --origin, the command sends the token to the address in TURNZERO_CLOUD_ORIGIN, or to https://turnzero.ai where that variable is not set. Where the line has --origin, it must be https://turnzero.ai or the address in TURNZERO_CLOUD_ORIGIN. An address on the machine itself is allowed only where TURNZERO_CLOUD_ORIGIN names it. Any other --origin ends the command with status 3 before anything is sent. So a line copied from somewhere else cannot send your token to an address your job did not set.

A run under a deploy code does not read TURNZERO_CLOUD_ORIGIN: the code goes to the line's --origin, or to https://turnzero.ai where the line names none. deploy never reads TURNZERO_CLOUD_TOKEN, your application's own credential.

Storing a secret

secret store stores a secret under a name, and secret rotate replaces the value stored under a name. Your tool calls store_secret or rotate_secret with no value, and the response has the line, in its two forms. The line pipes a secret grant to the command on its input. The grant is for one write: that action, that name, and that scope. Store a secret describes the call and says who runs the line.

The value comes from the file --value-file names, or from a hidden prompt at the terminal under --value-prompt. A line has exactly one of the two, and the value is never an argument. The command reads the file as UTF-8 text, drops a byte-order mark and one line ending at the file's end, and refuses a UTF-16 file.

The prompt needs a terminal of your own, one that also shows the command's output, and it waits at most five minutes. Where the command's output goes anywhere else, as in an AI tool's shell, the command refuses the prompt at once and sends nothing.

The command sends the value in one write, which spends the grant, and follows no redirect. It never makes the write again, because the first request may have reached the platform. A second run of the same line is refused secret_grant_spent, and a new call from your tool returns a fresh line.

Uploading a file

put uploads one file to a storage area of your application. Your tool calls mint_upload_grant for the application, the area, and the file's name, and the response includes an upload grant for that one file. The response has the put line in its two forms, command and command_windows. Your tool runs the one for its operating system, as given. The line pipes the grant to the command on its input, and names the area with --area, the file's name with --name, and the file on your machine with --path.

The response has no line where the file's name contains a character that one line cannot pass unchanged through every shell, such as a space. An upload grant from your tool describes the call, the line, and the two commands the response has for that case.

The command sends the file as it is, in one write, and follows no redirect. The grant covers that one write, so a second upload takes a new grant. A write of a name that is already stored replaces that file. Store files describes the areas.

Downloading an export

export download downloads every file of one completed export into a folder on your machine. Your tool calls mint_download_grant for the application and the export, and the response has the line in its two forms, command and command_windows. Your tool runs the one for its operating system, as given. The line pipes a download grant to the command on its input, and names the application and the export with --export and the folder with --path. Download the export describes the call and the line.

The command reads the export's manifest, then each file the manifest lists, four at a time. It writes manifest.json, each table as database/<table>.csv, and each stored file as files/<area>/<name>. It writes each file under a temporary name first and moves it into place once every byte has arrived, so no half-written file has its final name. A file whose version differs from the one the manifest lists is kept as it arrived and reported as moved after the listing.

The download grant reads the export's own files and the application's stored files that were created by the time the export listed them. So a file that was rewritten after the export is downloaded with its newer bytes and reported as moved. A file that was deleted and stored again after the export is not downloaded: its read returns no_such_file, the command counts it as a failed file, and a new export includes it. Download the export covers a file first stored after the export, and an export made before this rule.

The folder can be new, empty, or one an earlier run of the same export wrote. The command refuses a folder that has files and no index of this export. With no --path, it writes a new folder, export-<export id>, in the folder it runs in. It refuses to run there where that folder has a package.json, because a deploy from that folder would include the export.

An index file in the folder, .turnzero-cloud-export.json, records each finished file. A second run skips every file the index records whose size on disk matches. A read does not use the grant up, so the same line runs again until the grant expires, and a new mint_download_grant call returns a fresh line for the same folder after that.

Where the platform returns 429 rate_capped, the command waits for the next minute and reads again, for at most five minutes in one run. It sends no environment header, because the grant fixes the environment, and it follows no redirect.

Taking a library entry

library take takes one entry of the library into your project, and Use the library describes the library. It needs no credential, because the library's reads are open.

Your tool calls read_library_entry with the entry's name, and the response's download has the library take line in its two forms, command and command_windows. Its next says what remains after the line: the commit, the packages rows, and, in a Turn Zero Blueprint project, the version in the instance file. Add an entry to the library folder shows the line.

Run it at your project's root. That is the folder that contains system/ or system.json beside app/, or, on a first take, the folder where the new system/ is made. Where the folder contains neither and the folder above it contains one, the command refuses and writes nothing, because the folder may be an application's folder inside that project. The printed sentence gives both ways to continue. Run the line in the folder above. Or, where the folder is a project of its own, make an empty system/ folder in it and run the line again.

The command reads the entry's file list and each file. A read that returns HTTP 502, 503, or 504 is made again after 2, then 4, then 8 seconds, at most three more times. It checks each file's SHA-256 and the entry's closure hash, and it writes nothing until every file is checked. It then writes the entry's row in the library folder's manifest.json and replaces the entry's folder under that folder whole.

For an entry with compiled modules, the command also copies the entry's package.json and lib/ into your application's folder and runs npm install there. Where the application's package.json already declares the copy at a file: path, the command replaces the copy at that path. Where it does not, the command writes the copy beside your code and declares it:

  • where the package file is app/package.json, the copy is app/lib/<name>, declared file:lib/<name>;
  • where the package file is at the project root and the project has an app/ folder, the copy is app/lib/<name>, declared file:app/lib/<name>;
  • where the package file is at the project root and there is no app/ folder, the copy is lib/<name>, declared file:lib/<name>.

Where neither app/package.json nor a package.json at the project's root exists, the command refuses, writes nothing, and says to make your application's package.json first and run the line again. For an entry a template takes, in a project with no app/ folder or an empty one, it also gives that template's scaffold line, which writes a new service and takes the entry. The Database entry gives the database-service line, and the Schedule entry the scheduled-job line.

npm install receives neither TURNZERO_CLOUD_MINTED_TOKEN nor TURNZERO_CLOUD_UPLOAD_GRANT. If the install fails, the command ends with status 1, and the entry, its row, and the copy stay written. Run the line again to take the entry again whole.

The command refuses, with nothing written, a library folder, an entry's folder, or a copy that is reached through a link leading outside your project.

Running on your machine

provision writes the settings a local run needs into .env, the environment file in the folder it runs in. It re-mints the application's development platform credential, and its development database credential where the application has a development database. Run locally lists the seven settings it writes. An existing file keeps every line the command does not set and its permission mode, and provision writes no PORT.

Your tool gets the line from submit_manifest. A call that names local_run: true returns provisioning.command for macOS and Linux, provisioning.command_windows for Windows, and provisioning.expires_at. The line reads echo <grant> | npx -y <address> provision --application <application id>. It contains a secret grant, which lasts five minutes. Run the line for your system once, as given, in the folder the application runs in.

The grant re-mints each of the two development credentials once. A second run of the same line is refused secret_grant_spent and changes nothing. For a new line, call submit_manifest again with the application, its manifest, and local_run. Each such call returns a new line.

Through the MCP tool, a call that names no local_run returns provisioning with for and next, which says to submit again naming local_run: true, and creates no grant. Over the HTTP API, a call that names local_run returns the line and withholds both credential values, and a call that names none returns no provisioning.

With a development database, the command writes seven settings. Without one, the platform answers the database request not_found, which uses nothing of the grant, and the command writes the other five.

In an unattended run, set TURNZERO_CLOUD_MINTED_TOKEN to a token minted for the application, pipe no grant, and name --application <application id>. The platform refuses the application's own TURNZERO_CLOUD_TOKEN for this work. The token goes only where Deploying unattended says a deploy's token goes: https://turnzero.ai, or the address in TURNZERO_CLOUD_ORIGIN, an address on the machine itself only where that variable names it.

provision writes the platform's settings alone. A setting your manifest's settings member binds to a stored name gets no line, because no call answers a stored value. Give each one a local value yourself, on a line of your own in .env, which provision keeps, or in your shell. Run under the line's grant, provision prints a line naming each one.

The command writes the file whole under another name beside it, .env.turnzero-cloud-writing for .env, and renames that file into place, so a run that fails leaves .env as it was. A deploy leaves that file out, as it leaves out .env. The command tries the write before it re-mints anything, so a file it cannot write ends the run with status 3 while the credentials are still valid. It removes the file beside .env where the run ends before the rename, and where an interrupt ends the run.

Each run revokes the development credentials the application had before it. A deployed development environment keeps the revoked values until its next deploy, so deploy it again afterwards. The command prints each credential setting as a count of characters, never as a value. A run makes each re-mint once and never repeats it, because the first request may have reached the platform.

run starts your application as the platform starts it, with npm start in the folder it runs in or the folder --path names. The application receives your shell's environment with every setting of the environment file laid over it, and the file wins where both set the same setting. --port sets PORT. The command leaves TURNZERO_CLOUD_MINTED_TOKEN and TURNZERO_CLOUD_UPLOAD_GRANT out of what the application receives. It reads no credential and sends no request.

npm start passes on your shell's environment alone, so an application that reads a setting from .env, and loads no file in its own code, starts through run. Without --port, the application receives PORT only where .env or your shell sets it, and otherwise listens where its own code chooses. run prints the port only where --port names it.

A local run reads and writes the development database, where the application has one. A migration your application applies at a local start is recorded there, so the next deploy to development finds none pending. Production's database is separate, and the first start there applies them.

The command reads the file by these forms, and no other:

  • NAME=value. The name is a letter or an underscore, then letters, digits, and underscores. Spaces and tabs before the name and around the = are dropped, and so is the word export before the name.
  • A comment: a line whose first character after any spaces and tabs is #.
  • An empty line, or one that holds spaces and tabs alone.

The file is UTF-8 text. A byte-order mark at its head and a carriage return before a line's end are dropped. A name set on two lines takes the later line's value. A line of another form, or a folder with no package.json, ends the run with status 3 before anything starts. A file that is not there is read as no settings.

A value is everything after the first = to the end of its line, with the spaces and tabs at its two ends dropped. One pair of matching quotes, " or ', around the whole value is dropped. Nothing else in a value is interpreted, so a # after a value is part of the value.

The application's output passes through. The command replaces a credential the application prints with its setting's name between angle brackets. It replaces the value of TURNZERO_CLOUD_TOKEN and of APP_DATABASE_URL. For every setting whose value is a connection string that contains a password, it replaces the whole value and the password, both as the value spells it and with its percent escapes decoded.

The values are the ones the application receives, whether the environment file or your shell set them. The command finds no value printed in any other form, and replaces no value shorter than eight characters, so keep a value out of your application's output.

While the application runs, an interrupt does not end the command before the application ends. A terminal passes an interrupt to the application itself. On macOS and Linux a termination or a hang-up is passed on to the application as it is. The command passes a first interrupt on only where its standard input is not a terminal, since a terminal hands it to the application itself, and a second interrupt on as a termination.

On Windows the command passes nothing on: the application runs on the command's console, which hands it each interrupt itself. After an interrupt there, cmd asks Terminate batch job (Y/N)? once where the line was started through npx.cmd, as npx is in cmd, after the command has ended. Type N or Y and press Enter: either returns your prompt, and the command's last line says so. After N the line ends with the command's status, and after Y with cmd's own.

The command's last line follows an empty line, so it starts on a line of its own whether cmd's question comes before it or after it.

The question is npx.cmd's own. The command adds no question of its own where it finds npm beside Node.js or beside the npm that ran it, as on an ordinary install. Otherwise it starts npm through cmd, which asks once more and does not wait for a reply. A shell whose npx is not a batch file asks nothing: Git Bash, or PowerShell where scripts may run, with npx in place of npx.cmd.

A tool that ends the command's process outright, with no console interrupt, leaves the application running on Windows: end it by its port or in Task Manager.

Starting a new service

scaffold writes a new service from a template into your project and tests it, so your tool starts from a working service. The line, in its macOS and Linux form:

npx -y https://turnzero.ai/packages/turnzero-cloud-<version>.tgz scaffold

The command has three templates:

  • hello-world, the smallest service that deploys, with a health route and one API route that answers hello world.
  • database-service, a notes service that keeps its data in the platform's database, the service Build a database-backed service shows file by file.
  • scheduled-job, the hello-world service with one handler that the platform runs each hour through the Schedule package.

A line that names no --template writes hello-world, so the shortest line writes the smallest service. To write another, add --template and its name, such as --template database-service. The default stays hello-world: a template added later never becomes the default, so a line you use today keeps writing what it writes.

The command needs no credential. A database-service or scheduled-job run takes a package from the library, whose reads are open, and a hello-world run sends no request.

Run the line at your project's root, or name the root with --path, and the command makes that folder where it does not exist. On Windows, a path written into the line contains none of the characters &, |, <, >, ^, and %: where your folder's path contains one, run the line from that folder without --path.

The command starts a new application and writes over none. Where your project's app/ folder contains any file or folder, the command refuses and writes nothing. The command looks for an application nowhere else, so where yours is in another folder, the line writes a new, separate service into app/. The command has no option that overwrites, and it asks no question. It reads no other file in the root but system.json, and changes no file that exists there, a package.json included.

Under database-service and scheduled-job, the refusal's last line gives the library take line, which adds the template's package to the application you already have. That line also names the page to follow: the guide Add a database for the Database package, and the Schedule package's page for the Schedule package.

The command works in this order:

  1. Where the template takes a package, the command takes it as library take does: the Database package for database-service, and the Schedule package for scheduled-job. It reads the entry's file list and each file, and checks each file's SHA-256 and the entry's closure hash. It writes nothing until every file is checked. For hello-world, it reads your library folder as it stands, sends no request, and refuses where that folder cannot be used, saying what to repair.
  2. It makes the root, where --path names a folder that does not exist. Where the root contains neither system/ nor system.json, it makes a system/ folder there, whatever the folder above contains.
  3. It writes the template's files into app.scaffold beside app/ and renames that folder to app/. So app/ contains the whole template or none of it. An earlier run's files there under the same names are replaced. Where something else is at app.scaffold, such as an earlier run's files under another template or another resource, the command refuses, never removes it, and names the folder to move or remove.
  4. Where the root has no AGENTS.md, it writes one there, of one sentence.
  5. Where the template takes a package, it writes the package's row in system/manifest.json, the package under system/, and its compiled code in app/lib/database/ or app/lib/schedule/.
  6. It runs npm install and then npm test in app/.

Under every template, the command composes manifest.json at the run. It fills that file's packages list with the name and version of each entry in your library folder, and leaves the list empty where your project holds no entry.

app/AGENTS.md tells an AI coding tool that the service runs on Turn Zero Cloud and where the platform serves its rules. A tool started at your project's root reads the root's AGENTS.md first. So the command writes one there, only where the root has none, with one sentence: "The service in app/ runs on Turn Zero Cloud: read app/AGENTS.md before you work on it."

Where the root already has an AGENTS.md, the command changes nothing there and prints a not changed: line with the sentence to add. Where the write fails, it prints a write failed: line with the same sentence. Where the root has a CLAUDE.md, it prints a not changed: line for that file: Claude Code reads it and then no AGENTS.md on its own, so add the sentence to it. The command reads neither file, and writes no CLAUDE.md.

npm install and npm test receive neither TURNZERO_CLOUD_MINTED_TOKEN nor TURNZERO_CLOUD_UPLOAD_GRANT. An interrupt reaches npm, and the command ends after npm does. After an interrupted install, the tests are not started. On Windows the command starts npm as run does, and after an interrupt a line says that cmd may ask Terminate batch job (Y/N)?. A signal that arrives after the command's first write and before npm starts is acted on once the writes are made. The command then prints the way to continue, starts no npm, and ends with status 128 plus the signal's number.

After the tests pass, the command prints the template's next steps in order, which each template's part below lists. Under every template the first step is create_application, which tells your tool to skip it where it already created the new service's application. Where it printed a not changed: or a write failed: line, the steps begin with one step for each such line, which repeats the sentence to add. Each line it prints for the command includes the address of the platform the run used, and npx.cmd on Windows.

A new application has one environment, production, so the deploy step goes straight to production. The deploy call's answer says so before you run its line. To try each version on development first, call create_environment before you deploy, and follow its answer.

Where the install fails or the run stops, the last printed line gives one way to continue, by what is at app:

  • Where the run moved the template in, run npm install and then npm test in app/ after a hello-world run. After the other two, run the library take line the command prints, at your project's root, then npm test in app/.
  • Where it did not, and app/ does not exist or is empty, run the same line again.
  • Where something that is not a folder, or that the command cannot read, is there, move it or make it readable, then run the same line again.
  • Where app/ contains files that are not the run's, an application already exists there. Under database-service and scheduled-job, the line gives the library take line, as the refusal of such a folder does. After a hello-world run, the line says that scaffold starts a new application and writes over none, and gives no line to run.

Where the tests fail, they failed on the files as written, before any edit of yours. The printed line then asks you to report it, and gives no line to run again. It names the package and the version the library served, or, after a hello-world run, the template and the command's version.

The hello-world template

hello-world writes seven files into app/: package.json, manifest.json, src/server.mjs, src/main.mjs, test/hello.test.mjs, .gitignore, and AGENTS.md. The server answers GET /health with status 200 and {"ok":true}, and GET /api/hello with status 200 and the plain text hello world. It answers any other request with status 404 and {"error":"not_found"}. It listens on the port in PORT, or on 8080 where PORT is not set.

The route sits under /api/ so that it stays your application's own if you later add a web client, whose files the platform serves at /. The manifest declares no service, so the application needs no database and no storage area. The package file declares no dependency, and its two tests each read one route.

A hello-world run takes no package and sends no request. It ends with four next steps: create_application; submit_manifest with app/manifest.json, naming no local_run, because the service reads no setting; the run line; and deploy. After the run line, GET /api/hello answers hello world on your machine.

The database-service template

database-service is the notes service that Build a database-backed service shows file by file, written by the line with --template database-service.

The files in app/ are package.json, manifest.json, migrations/0001_notes.sql, src/db.mjs, src/server.mjs, src/main.mjs, test/notes.test.mjs, .gitignore, and AGENTS.md. The run copies the Database package's compiled code into app/lib/database/.

The server answers GET /health, GET /notes, and POST /notes. It answers any other request with status 404 and {"error":"not_found"}, a request whose target does not parse among them.

The resource options are this template's alone. To keep a resource other than notes, add --resource with its plural, the word of the table and the route, such as --resource orders. --singular names the singular, the word for one row. Without --singular, the singular is the plural without its closing s, so a plural that does not end in s needs it: --resource people --singular person. Each name is a lower-case ASCII letter followed by lower-case ASCII letters and digits, at most 30 characters.

The command then writes the same files with the resource's names in place of notes, note, Notes, and Note, in one pass over each file's path and text. So a name that contains another, such as footnotes, is written once. addNote becomes addOrder, the table "notes" becomes "orders", and test/notes.test.mjs becomes test/orders.test.mjs. A line that names no resource writes the files exactly as the command carries them.

The command ends with status 3 before it sends any request, and writes nothing, where a name is outside that form or the plural is health, the template's own route. It does the same where the plural has no closing s and no --singular, where the plural is s alone, and where --singular comes without --resource. The printed sentence names the option and what it takes. Under hello-world or scheduled-job, either option ends the command with status 3 before it writes anything, and the sentence says to leave it out or name --template database-service.

A database-service run ends with four next steps: create_application; submit_manifest with app/manifest.json, naming local_run, and the provision line its answer gives; the run line; and deploy.

The scheduled-job template

scheduled-job is written by the line with --template scheduled-job. It writes eight files into app/: package.json, manifest.json, src/server.mjs, src/main.mjs, src/jobs.mjs, test/job.test.mjs, .gitignore, and AGENTS.md. The run copies the Schedule package's compiled code into app/lib/schedule/, which the package file declares as file:lib/schedule.

The server answers GET /health and GET /api/hello as hello-world's does, and any other request with status 404. Ahead of those routes, the Schedule package's router answers the schedule's path, /jobs/heartbeat, and passes each scheduled run to the handler in src/jobs.mjs. A request to that path without the platform's headers gets status 404 and the error scheduled_handler_only. Where the manifest and the handlers do not match, the router stops the service as it starts.

The manifest declares one schedule, heartbeat, due once an hour. The minute in its cron expression is the minute of the hour, in UTC, at which you ran the line: a service written at 14:17 UTC is due at 17 minutes past each hour. The minute differs from service to service so that the services written from this template are not all due at the same moment. Any minute works, and the cron expression is yours to change to any timetable your plan's shortest interval allows.

The handler prints one line naming the schedule and the run's key, such as schedule heartbeat ran, key heartbeat@2026-01-01T09:00:00.000Z. It keeps no record of the runs it has handled, because printing a line is safe to repeat. A handler whose effect must not repeat records the key first, as Schedule describes.

The four tests read the two routes, fire the schedule through the package's test double, and send the schedule's path a request without the platform's headers. No schedule fires on your machine: the tests fire the handler there.

A scheduled-job run ends with five next steps: the four of hello-world, whose run step also says that no schedule fires on your machine, and a fifth, run_schedule. That step is how you read that a run happened:

  • After you deploy, call run_schedule with the application, the schedule heartbeat, and wait_seconds. It fires the handler now and answers how the run ended.
  • Where the run has ended, the answer's detail gives the read_logs call that shows the handler's line, under the source container. The source platform holds the run's event, not the handler's line.
  • read_schedules lists every run of the schedule with its outcome, the hourly runs among them.

Where you meant another template

A line that names no --template prints a template: line before the files, saying it wrote hello-world. Where you meant another template, remove the app/ folder that run wrote. Remove the root's AGENTS.md too where that run printed written: AGENTS.md. Then run the line again with --template and the name you meant. Leave system/ as it is: the next run uses it.

Do not follow the refusal that a second run prints while app/ still holds the hello-world files. Its library take line would add the package to the hello-world service rather than write the template you meant.

Options

An option's value that opens and closes with a double-quote character is read without that pair, because a shell on Windows can leave the quotes in place.

The deploy subcommand takes these options, each as the line gives it:

Option What it sets Where absent
--application The application to deploy, by its identifier, a lowercase UUID, as the line a deploy call returns names it. Required
--origin The platform's address. Under every subcommand that takes the option, it is an https address with no path, or an http address on your own machine. Its host has only letters, digits, dots, and hyphens, or is an IPv6 address in brackets. Under a minted token, the address is the one Deploying unattended allows. https://turnzero.ai
--path The folder to zip, or a .zip file to upload as it is. The folder the command runs in
--rotate-database-credential Takes no value. Rotates production's database credential with this deploy. The line has it where the deploy call named rotate_database_credential. No credential is rotated
--env The environment to deploy to, development or production. The line a deploy call returns names none, and an unattended line may name one. The platform chooses, and the command deploys to the environment the preparing call returns

The platform checks whether the upload's grant allows a start in the environment that --env names. Where the line names no --env, the start names the environment the preparing call returned. Where the platform refuses the start, the upload is already stored and the command ends with status 3. The way on is a new deploy call and its line, or under a minted token the same line again.

The hash subcommand takes two options. It sends no request, so it takes no --origin, and any other option ends it with status 3:

Option What it sets Where absent
--path The folder to zip as deploy zips it, or a .zip file to hash as it is. The folder the command runs in
--list Takes no value. After the hash and any warnings, prints one line for each file the zip holds, with its size and its name. It lists a folder's zip only: with a --path that names a .zip file, it ends with status 3 and prints no hash. No list is printed

Where your application's folder or zip is somewhere else, name it with --path. A path you write into a line has none of &, |, <, >, and ^. PowerShell passes a path that has no space to npx.cmd without its quotes, and cmd then reads those characters as command syntax. A line that a tool call returns has no such path, because the call refuses one.

The secret subcommand takes these options, each as the line gives it:

Option What it sets Where absent
--name The name the value is stored under. Required
--application The application whose scope has the secret. The account's scope
--environment The environment of that application whose scope has the secret, development or production. It goes with --application. With no --application, none
--value-file The file that contains the value. Only where the line has --value-prompt
--value-prompt Asks for the value at the terminal without showing it. It takes no value of its own. Only where the line has --value-file
--origin The platform's address. https://turnzero.ai

The put subcommand takes these options, each as the line gives it:

Option What it sets Where absent
--area The storage area the file lands in, one of the application's. Required
--name The name the file is stored under in the area. Required
--path The file on your machine to upload. Required
--origin The platform's address. https://turnzero.ai

The export download subcommand takes these options, each as the line gives it:

Option What it sets Where absent
--export The export to download: the application's identifier and the export's identifier, joined by /. Required
--path The folder to write into. A new folder, export-<export id>, in the folder the command runs in
--origin The platform's address. https://turnzero.ai

The library take subcommand takes these options:

Option What it sets Where absent
--entry The library entry to take, by the name list_library returns, such as storage or ui/styles. Required
--path The project's root: the folder that contains system/ or system.json beside app/, or, on a first take, the folder where the new system/ is made. The folder the command runs in
--origin The platform's address. https://turnzero.ai

The provision subcommand takes these options:

Option What it sets Where absent
--application The application whose development settings are written, by the identifier create_application returned. Required
--env-file The environment file to write. .env in the folder the command runs in
--origin The platform's address, which is also written as TURNZERO_CLOUD_API and TURNZERO_CLOUD_GATEWAY_URL. https://turnzero.ai

The run subcommand takes these options:

Option What it sets Where absent
--path The application folder where npm start runs. The folder the command runs in
--env-file The environment file whose settings the application receives. A file that is not there is read as no settings. .env in the folder the command runs in
--port The port the application listens on, a whole number from 1 to 65535, set as PORT over the file's line and your shell's. The command sets no PORT

The scaffold subcommand takes these options:

Option What it sets Where absent
--template The template to write. hello-world is the smallest service that deploys, whose one API route, GET /api/hello, answers hello world. database-service is a notes service that keeps its data in the platform's database. scheduled-job is the hello-world service with one handler, which the platform runs each hour through the Schedule package. hello-world
--resource Under database-service, the resource the service keeps, by its plural, such as orders: the word of its table and its route. A lower-case ASCII letter, then lower-case ASCII letters and digits, at most 30 characters, and never health. Another template refuses it. The service keeps notes
--singular Under database-service, the resource's singular, such as order, in the same form. It goes with --resource. Another template refuses it. The plural without its closing s
--path The project's root, where app/ and system/ are written. The command makes the folder where it does not exist. The folder the command runs in
--origin The platform's address. https://turnzero.ai

--version prints the command's version, and --help prints its usage.

What it reads

  • The credential. A subcommand that needs one reads a grant from its input, as the line pipes it in, and stops reading after ten seconds. Under deploy, what the line pipes is a deploy code, which has a grant's form. A value that is not of its form stops the command before any request.
  • The minted token. Under deploy and provision, where nothing was piped, the command reads the token in TURNZERO_CLOUD_MINTED_TOKEN. Where the input holds a value, the command does not read the variable, whether or not the value has its form. It sends the token only to https://turnzero.ai or to the address in TURNZERO_CLOUD_ORIGIN, an address on your own machine only where that variable names it.
  • How a credential is handled. The command takes the credential it runs under in none of its arguments and from no file, writes it to no disk, and never prints one. deploy never reads TURNZERO_CLOUD_TOKEN, your application's own credential. The command sends a credential only to an https address, or to an address on your own machine. No request that includes a credential follows a redirect.
  • Your application's folder. The folder must contain package.json at its top, and that folder becomes the zip's root. The command leaves out every node_modules and .git entry and every .env and .env.* file, whatever the case of their names. So dependencies and .env files stay on your machine, and hash and deploy print what was left out. The command leaves out nothing else, since a file left out may be one your code loads at run time; to ship less, name your own .zip file with --path.
  • The zip it builds. The command writes / between the parts of each name in the zip. It stops at a symbolic link it would otherwise include, and reports it, because the build would not follow it. It also stops at a folder whose zip would contain more than 65,535 files. The same folder always zips to the same bytes, and a .zip file named with --path is uploaded as it is. Every version of the command builds the same zip from the same folder on the same Node.js version, so a hash printed by one version matches the upload of another.
  • Your application's Git commit. Where the folder that deploy zips is inside a Git repository, the command reads the commit checked out there from Git's own files, with no git program needed. It sends that commit with the start, and the platform keeps it with the version it deploys. The command sends the commit as it is, without changes you have not committed. Where no commit can be read, or --path names a .zip file, the command sends none and the deploy goes on as before. hash reads no commit.
  • The files you name. secret reads the file --value-file names. put reads the file --path names, whole. provision and run read the environment file. library take reads the library folder, its manifest.json, and your application's package.json.
  • TURNZERO_DEPLOY_NO_WAIT. Where this environment variable has any value, the command returns once the deploy has started, and read_status shows the outcome.
  • TURNZERO_CLOUD_USER_AGENT. Where this environment variable is set, its value is added to the user agent the command sends, turnzero-cloud/<version>, so your own records can tell your runs apart. The value takes printable characters only, and any other character ends the command with status 3 before any request, under every subcommand that sends one.

What it prints

The command prints plain lines. hash prints sha256: followed by the hash in 64 lower-case hexadecimal characters on its first line:

sha256: <64 hexadecimal characters>

For a folder with nothing more to report, and without --list, that is its only line. After it, hash prints up to four more lines, each only where it applies:

  • left out: with the paths the zip left out, each relative to the folder, a folder with a closing slash. The line lists at most ten paths, in order, and then the count of the rest.
  • warning: with the name of each .zip file at the top of the folder. The upload includes such a file, so keep a build's zip outside the folder unless your application reads it. A .zip file in a folder below the top draws no warning.
  • warning: with the name of each file in the zip that looks like a key or a credential. The upload includes such a file. Where one holds a secret, move it out of your application's folder before the next deploy, and store a value your application needs with store_secret, as Storing a secret describes. The line lists at most ten names, in order, and then the count of the rest.
  • warning: with the health path of manifest.json, where no other file in the zip contains the path's last part, in its text or in its own path, such as /health for /api/health. The path / draws no warning, and a path longer than 80 characters is printed cut short. A deploy waits for that path to return 200, so check that your application serves it. The command cannot see a route built in another form, so this line is a warning and never a refusal. The platform refuses a first deploy on a narrower reading, as Author the manifest describes.

None of these lines changes the status or the zip. A name in them has each character you would not see, such as a line ending, replaced by a percent escape. A name longer than 80 characters is cut short, and the health path is printed without its query.

A file looks like a key or a credential where one of three things holds, whatever the case of its name and wherever it stands in the folder:

  • Its name is id_rsa, id_dsa, id_ecdsa, id_ed25519, .netrc, .pgpass, .htpasswd, or credentials.json, or it starts with client_secret and ends .json.
  • Its name ends .key, .p8, .p12, .pfx, .jks, .keystore, .ppk, or .env.
  • It holds a private key written as text, under any name. Such a key starts with a line of five hyphens, the word BEGIN, sometimes the kind of key, and the words PRIVATE KEY, and the key's own letters and digits follow it. A file that only mentions that first line, such as a page of documentation or a line of code, is not named.

The line never shows what a file holds. A .pem file that holds only a certificate is not named, and a file that holds a whole key as an example is.

With --list, hash then prints one line for each file the zip holds, in the zip's order. Each line is file:, the file's size in bytes, and its name, whole and never cut short. In the name, each character you would not see is replaced by a percent escape. hash reads no grant and sends nothing, so hash --list is the preview of what a deploy from the folder would upload.

For the health path, the command sets aside package files and documentation, and the warning line says so. It does not read package.json, a lock file, or a file whose name ends .md, .markdown, .txt, or .rst. The lock files are package-lock.json, npm-shrinkwrap.json, yarn.lock, and pnpm-lock.yaml. A README.md or a package description that names the path therefore does not stop the warning. A file with one of those endings still counts where its own path ends with the whole health path, such as public/health.txt for /health.txt.

deploy prints these lines, in this order:

  • zip: with the number of files and bytes zipped from the folder, or the .zip file uploaded as it is, then sha256: and the zip's hash, the same hash hash prints;
  • the left out: line and the warning: lines, as hash prints them, each only where it applies;
  • where it withdraws a code before it refuses the folder, a line that opens with withdrawn:;
  • prepared: with the upload's identifier and when its grant expires, before the upload starts, so the identifier ties the deploy to your run;
  • uploaded: with the bytes uploaded under the grant and the upload's identifier;
  • started: with the platform's response to the start, on one line. Where the call starts the deploy, it includes health_path, the manifest's health path the check will probe, so a wrong path shows before the image build ends;
  • then, as the deploy's progress arrives, one line for each step the deploy enters, such as step: image_build at 0s;
  • one line with the outcome, such as settled: deployed version 2 or settled: failed at health_gate: health_gate_failed;
  • after settled: deployed, a checked: line saying the health check requested the health path alone, so request your other routes and read read_logs for errors;
  • on a failure, the platform's reading of the likely cause and the health check's facts, never your application's own output;
  • last, a line that gives the read_status call, which returns the whole record.

The warning for a file that looks like a key or a credential refuses nothing, so under deploy the upload it reports is already under way. Where the file holds a real key, replace that key with a new one, because a deployed version keeps the files of its zip. Then move the file out of the folder and deploy again with a new deploy call and its line.

The figure after at on a step line counts seconds from the deploy's start. Each step line after the first also says how long the step before it took, as in step: compute_apply at 40s, image_build took 40s. A step your manifest needs nothing from is still listed and ends at once.

While it waits, the command reads the progress at most 450 times. Each read is bounded at 60 seconds, and the command pauses two seconds after a read that brings nothing new. A deploy usually ends within four minutes and has taken more than six, and the command stops waiting fifteen minutes after the deploy starts. So where your tool limits how long a shell command runs, allow this one fifteen minutes and the upload's time. Where the platform refuses the upload or the start, the command prints the refusal's name and explanation as the platform gave them.

A progress read that returns status 502, 503, or 504 has met a failure that is often temporary. The command prints a line that says so and makes the read again after 2, then 4, then 8 seconds, at most three more times for one read, each counted among the 450. A read that still fails ends the wait with status 2. The command makes the upload and the start once each, and never again, because either may already have taken effect.

The other subcommands print these lines:

  • secret prints one line that opens with stored: or rotated: and gives the secret's name and scope as JSON, then the platform's explanation. It prints neither the value nor the grant.
  • put prints one line that opens with put: and gives the file as the area stores it, as JSON.
  • export download prints the name of each file it writes under an escaped name or finds moved. It ends with one line that opens with downloaded: and counts the files on disk. That line also counts the files read in this run, already on disk, moved, failed, and not read.
  • library take prints a taken: line; for an entry with compiled modules, a copied: line and an installed: line; then a packages: line and a next: line.
  • provision prints the environment file's settings, each credential as a count of characters, then a line about the development database.
  • run prints up to four lines of its own, then the application's output. They show the folder, the settings the file sets, any setting where the file wins over your shell, and --port. They print no value.
  • scaffold, where the line names no --template, first prints a template: line. It says the command wrote hello-world because the line names none, and that Starting a new service says what to do where another was meant.
  • scaffold then prints a written: line for each file it writes, at the path it writes it at, and a not changed: line for a root file it leaves as it is. Where the write of the root's AGENTS.md fails, it prints a write failed: line. Under a template that takes a package, it then prints the taken: and copied: lines of library take, and a hello-world run prints neither. Then come npm's output, an installed: line, and a tested: line, and the next steps, numbered, after a line that opens with next:.

A refusal prints a line of the form <act> refused (HTTP <status>): <error>, followed by the platform's explanation. A failure of the platform reads <act> failed in its place. A refusal of the command's own identifies the option and never repeats the value it was given, so a value typed into the wrong place is not printed.

The preparing call's refusals print under the act prepare. A deploy code that is spent, expired, or unknown is refused deploy_code_refused. The way on is a new deploy call from your tool, whose response says what became of the old code. Where the platform refuses the call rate_capped, the same line may run again after the wait the response names. No line the command prints holds a deploy code, a grant, or a minted token.

Two refusals of the command's own say what to do next:

  • Under a deploy code, where deploy finds no application folder, the command withdraws the code, and the refusal also names local_path. Call deploy from your tool again with local_path, the folder's path, and run the line that call returns.
  • On Windows, the command refuses a path under --path, --value-file, or --env-file that still contains a double quote, since no Windows path contains one. The usual cause is a quoted path that ends with a backslash, which cmd and Windows PowerShell 5.1 read as keeping the quote. Write the path with no backslash at its end. On macOS and Linux the command takes the path as given.

How it ends

Status Meaning Next
0 A deployed version. With TURNZERO_DEPLOY_NO_WAIT set, a deploy that started. Under hash, the hash was printed. Under secret, the value was written. Under put, the file was written. Under export download, every file the manifest lists is on disk. Under library take, the entry was taken, and installed where it has compiled modules. Under provision, the settings were written. Under run, the application ended with status 0. Under scaffold, the files were written and installed, the template's package was taken where it takes one, and the tests passed. With the variable set, read_status shows the outcome. After scaffold, follow the next steps it prints.
1 The action ran and failed, in whole or in a part the printed lines identify. Under deploy, the deploy failed. Under export download, a file failed, and the same line run again reads again each file whose read failed. Where the platform returned HTTP 404 no_such_file for a file, the closing lines name a new export. Under library take, the entry, its row, and the copy were written, and npm install failed. Under provision, the settings were written without a connection limit, which the platform's response did not include. Under scaffold, the files were written, and npm install or the tests failed. The printed lines give the platform's reading, and read_status gives the whole record. After a failed install, run npm install in the application's folder. After provision, the last printed line says when to run it again. After scaffold, the last printed line gives the line to run, or, where the tests failed, asks you to report it.
2 The outcome is unknown, or the run stopped before its end. The command also ends with status 2 where it stops on an error of its own, and its last line says so. Under deploy, the command stopped waiting: a progress read was refused or got no response, or the reads ran out. Or the start got no response, or the platform failed while responding to it. The deploy may still be running. Under secret, the write got no response, or a response that is not a refusal, and the value may have been written. Under put, the upload got no response, or a response that is not a refusal, and the file may have landed. Under export download, the run stopped before every file was read, as when the grant expired. Under provision, a credential was re-minted, or may have been, and its value was not written. Under scaffold, the run stopped before its end: an interrupt or another signal ended npm, the command met an error of its own, or something came to exist at app while it wrote. Under deploy, read_status with its wait, as the last line says. Under secret, list_secrets shows when the name was last written. Under put, a new grant writes the file again. Under export download, a new mint_download_grant call returns a line that resumes in the same folder. Under provision, the printed sentence says which line re-mints again. Under scaffold, the last printed line says how to continue.
3 Nothing was started. An option, a variable, or the path could not be used, or the credential was missing or not of its form. Or the machine runs a Node.js older than 24. Or the folder had no package.json, contained a symbolic link, or had too many files. Under deploy, the preparing call was refused or returned no upload the command can use, the upload or the start was refused, or the upload got no response. Under hash, no hash was printed, because of an option it does not take or a folder or path that deploy would also refuse. Under secret, put, export download, and library take, nothing was written. Under provision, nothing was re-minted. Under run, no application was started. Under scaffold, nothing was written. The printed refusal or sentence gives the call to make next. Under a deploy code, that is a new deploy call from your tool and its line, or after a rate_capped refusal the same line once the wait ends. Under a minted token, it is usually the same line again.

run is the one exception. It ends with the application's own status, whatever that is, so a status under run may be the application's and not the command's. Where a signal ended the application, the status is 128 plus the signal's number. After an interrupt on Windows, the status is npm's, or cmd's where the command started npm through cmd, and may not be the application's. Where no application was started, run ends with status 3 and the command's own lines say so.

Deploying without the command

The command is the short way, not the only one. A script of your own can upload the zip to a storage area of its own and call deploy naming that file, as Deploying from an area of your own and Deploy describe.