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 thedeploycall that returned the line. It is never one of the command's own arguments.npx -yfetches the command from its address and runs it with Node.js 24 or later. The-yanswers yes to the question npm asks before it fetches a package.--applicationnames the application by its identifier. The line also has--pathwhere the call namedlocal_path,--originwhere the platform is at another address, and--rotate-database-credentialwhere the call namedrotate_database_credential.- The Windows form,
command_windows, hasnpx.cmdfornpx, 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 isapp/lib/<name>, declaredfile:lib/<name>; - where the package file is at the project root and the project has an
app/folder, the copy isapp/lib/<name>, declaredfile:app/lib/<name>; - where the package file is at the project root and there is no
app/folder, the copy islib/<name>, declaredfile: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 wordexportbefore 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, thehello-worldservice 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:
- Where the template takes a package, the command takes it as
library takedoes: the Database package fordatabase-service, and the Schedule package forscheduled-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. Forhello-world, it reads your library folder as it stands, sends no request, and refuses where that folder cannot be used, saying what to repair. - It makes the root, where
--pathnames a folder that does not exist. Where the root contains neithersystem/norsystem.json, it makes asystem/folder there, whatever the folder above contains. - It writes the template's files into
app.scaffoldbesideapp/and renames that folder toapp/. Soapp/contains the whole template or none of it. An earlier run's files there under the same names are replaced. Where something else is atapp.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. - Where the root has no
AGENTS.md, it writes one there, of one sentence. - Where the template takes a package, it writes the package's row in
system/manifest.json, the package undersystem/, and its compiled code inapp/lib/database/orapp/lib/schedule/. - It runs
npm installand thennpm testinapp/.
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 installand thennpm testinapp/after ahello-worldrun. After the other two, run thelibrary takeline the command prints, at your project's root, thennpm testinapp/. - 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. Underdatabase-serviceandscheduled-job, the line gives thelibrary takeline, as the refusal of such a folder does. After ahello-worldrun, the line says thatscaffoldstarts 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_schedulewith the application, the scheduleheartbeat, andwait_seconds. It fires the handler now and answers how the run ended. - Where the run has ended, the answer's
detailgives theread_logscall that shows the handler's line, under the sourcecontainer. The sourceplatformholds the run's event, not the handler's line. read_scheduleslists 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
deployandprovision, where nothing was piped, the command reads the token inTURNZERO_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 tohttps://turnzero.aior to the address inTURNZERO_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.
deploynever readsTURNZERO_CLOUD_TOKEN, your application's own credential. The command sends a credential only to anhttpsaddress, 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.jsonat its top, and that folder becomes the zip's root. The command leaves out everynode_modulesand.gitentry and every.envand.env.*file, whatever the case of their names. So dependencies and.envfiles stay on your machine, andhashanddeployprint 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.zipfile 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.zipfile named with--pathis 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
deployzips is inside a Git repository, the command reads the commit checked out there from Git's own files, with nogitprogram 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--pathnames a.zipfile, the command sends none and the deploy goes on as before.hashreads no commit. - The files you name.
secretreads the file--value-filenames.putreads the file--pathnames, whole.provisionandrunread the environment file.library takereads the library folder, itsmanifest.json, and your application'spackage.json. TURNZERO_DEPLOY_NO_WAIT. Where this environment variable has any value, the command returns once the deploy has started, andread_statusshows 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.zipfile 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.zipfile 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 withstore_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 ofmanifest.json, where no other file in the zip contains the path's last part, in its text or in its own path, such as/healthfor/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, orcredentials.json, or it starts withclient_secretand 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.zipfile uploaded as it is, thensha256:and the zip's hash, the same hashhashprints;- the
left out:line and thewarning:lines, ashashprints 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 includeshealth_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 2orsettled: failed at health_gate: health_gate_failed; - after
settled: deployed, achecked:line saying the health check requested the health path alone, so request your other routes and readread_logsfor 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_statuscall, 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:
secretprints one line that opens withstored:orrotated:and gives the secret's name and scope as JSON, then the platform's explanation. It prints neither the value nor the grant.putprints one line that opens withput:and gives the file as the area stores it, as JSON.export downloadprints the name of each file it writes under an escaped name or finds moved. It ends with one line that opens withdownloaded: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 takeprints ataken:line; for an entry with compiled modules, acopied:line and aninstalled:line; then apackages:line and anext:line.provisionprints the environment file's settings, each credential as a count of characters, then a line about the development database.runprints 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 atemplate:line. It says the command wrotehello-worldbecause the line names none, and that Starting a new service says what to do where another was meant.scaffoldthen prints awritten:line for each file it writes, at the path it writes it at, and anot changed:line for a root file it leaves as it is. Where the write of the root'sAGENTS.mdfails, it prints awrite failed:line. Under a template that takes a package, it then prints thetaken:andcopied:lines oflibrary take, and ahello-worldrun prints neither. Then come npm's output, aninstalled:line, and atested:line, and the next steps, numbered, after a line that opens withnext:.
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
deployfinds no application folder, the command withdraws the code, and the refusal also nameslocal_path. Calldeployfrom your tool again withlocal_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-filethat 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.