CLI
Change a project from your terminal or your coding agent, follow it until it is live, set up runs that repeat, and call any API route.
The syndai CLI is the owner's door to a project. You say what to change. Syndai builds it, runs every check, puts it live and prints the URL. It needs Node 22 or newer and nothing else. It sends no telemetry.
npx -y syndai@latest login
npx -y syndai@latest change "make the header blue"
Changing bakery.syndai.app · goes live when checks pass
· Working: Updating the header colour
· Checking and going live
Live: https://bakery.syndai.app
The rest of this page writes syndai for npx -y syndai@latest.
Log in
syndai login opens app.syndai.ai/cli with a one-time code. A workspace owner or admin approves it there. The CLI then gets a key named CLI · <your computer>. The key has the owner door scopes and lasts 90 days.
| command | does |
|---|---|
syndai login | browser approval; the key is stored for that workspace |
syndai login --scope <scope> | ask for one more scope on top of the ones you have; repeat the flag for more |
syndai login --with-token < key.txt | store a key you already have (read from stdin, never from a flag) |
syndai whoami | the workspace, key name, scopes and expiry in use, and where the key came from |
syndai logout | revoke the stored key and forget it |
When you log in again, the CLI saves the new key first. Then it revokes the old one. If that revoke fails, you get a warning and the new key still works.
In CI, set SYNDAI_API_KEY instead. It always wins over a stored login. There is no --api-key flag, so a key never lands in shell history.
Keys and scopes
A key can only do what its scopes allow. The key you get from syndai login has the Owner CLI and MCP scopes. They let you read and change projects, start and answer changes, put changes live, edit site text, change project settings and manage runs that repeat.
Some calls need a scope that key does not have. In a terminal, the CLI asks: This needs <scope>. Approve it in your browser? [Y/n] Yes opens the same approval page. Your new key keeps every scope you had, plus the new one. The CLI saves it, revokes the old key and runs your command again. No mints nothing.
The CLI does not ask when:
- the key comes from
SYNDAI_API_KEY. That key belongs to your CI, so the CLI never swaps or revokes it; - you passed
--no-input, or the output is not a terminal; - the command is
watchor another command that streams.
Then it prints the scope and the syndai login --scope <scope> command to run, and exits 5.
Things only a person can do
A key is access you hand to a tool. Some things stay with a person signed in at app.syndai.ai:
- landing a held change anyway;
- loosening a project's protected files;
- buying credits;
- managing keys or members;
- unpublishing an app.
No key can ask for these. The CLI prints the portal link instead. For a held change it exits 3 with the review page's URL.
Make a change
syndai change "add an opening-hours section under the menu"
The first line says where the change goes and what happens to it. The CLI then follows the change and exits when it has an outcome.
| option | does |
|---|---|
--app <project> | choose the project (see Choosing the project) |
--review | open a pull request for your review instead of going live; finish with syndai go-live <run> |
--image <file> | attach a PNG, JPEG, GIF or WebP (a mockup, a screenshot of the bug); repeatable |
--no-wait | start the change, print its state once, and exit 0 |
--timeout <duration> | stop following after 90s, 10m or 1h (exit 6); the change keeps going |
--no-input | never ask a question inline, even in a terminal |
A change is never made twice. Before the request goes out, the CLI saves its request id under $XDG_STATE_HOME/syndai/pending/. Say the reply is lost, or the process dies. Run the same syndai change again. It sends the same id, and Syndai answers with the change it already made.
Start a new project
syndai new "a booking site for my hair salon"
syndai new starts a project from scratch, like New project on Build. Name a product you want to learn from, and Syndai builds your own version of it:
syndai new "my own Calendly-style scheduler, like https://cal.com"
Syndai first reads that product's public pages. This takes about a minute. Then it asks which features to build first. The core ones are already checked, and you can name your version. In a terminal you answer inline. Press Enter to keep the checked features. If no one answers, it starts with the checked features after 24 hours. A GitHub link is not treated as a product to learn from.
| option | does |
|---|---|
--no-learn | treat the link as context only; build from your words |
--no-wait | start the project, print its state once, and exit 0 |
--timeout <duration> | stop following after 90s, 10m or 1h (exit 6); the build keeps going |
Follow a change
syndai watch <run> picks up any change, from any machine. It long-polls the change's state (GET /runs/{id}/change?wait=45). So it keeps going through proxies, restarts and Syndai deploys.
- Ctrl-C stops watching and prints
syndai watch <run>. It never cancels the change. --timeout <duration>exits 6 when time runs out. The change keeps going.--logsalso prints the change's events as they happen. With--jsonyou get one JSON object per line, then the final state.
When it needs you
| state | what to run |
|---|---|
a question (needs_input) | in a terminal the question is asked inline; otherwise syndai answer <run> |
held (held) | syndai review <run> |
ready for review (ready) | syndai go-live <run> |
syndai answer <run> asks the open question in a terminal. Scripts pass the answers with --input, from a file or from stdin (-):
echo '{"entries":[{"input_id":"shade","selected_option_ids":["navy"]}]}' | syndai answer $RUN --input -
syndai review <run> shows why a change is held, which files it touches and the diff. Then it asks Approve in your browser? [Y/n] Yes opens the review page and keeps watching until you decide. --retry runs the checks again and --close drops the change. Both act at once.
Landing a held change anyway, or loosening protected files, is only for a person. The CLI exits 3 with the review page's URL.
syndai go-live <run> puts a ready change live. syndai revert <run> undoes a change that went live (add --force when newer changes landed on top). syndai cancel <run> stops one.
Edit text in seconds
For an exact text swap on a live site, skip the coding run:
syndai edit-text --page /about --from "Open 9-5" --to "Open 8-6"
It is free and live in seconds. It prints the --undo <sha> command that puts the old text back. --all changes every match in the file that holds it. It is refused while a change is running on the site. Wait for that change, or make the edit with syndai change.
Runs that repeat
A schedule runs the same prompt on a project again and again, for example a weekly content refresh. Each run is a normal change. It shows on the project's Runs tab with a link back to its schedule.
syndai schedule add "refresh the specials on the home page" --every weekly --at "mon 09:00"
Repeats on bakery.
Weekly on Monday at 9:00 AM America/Chicago: "refresh the specials on the home page"
Next: Mon, Oct 19, 9:00 AM · Mon, Oct 26, 9:00 AM · Mon, Nov 2, 9:00 AM
Manage: syndai schedule show 3f2a9c1e-…
| command | does |
|---|---|
syndai schedule list [--app A | --all] | the project's schedules, or every project's with --all: when each runs next, or why it is paused |
syndai schedule show <id> | one schedule, its next 3 runs, and its history (time, what happened, the run id) |
syndai schedule add "<prompt>" --every … [--at …] [--tz Z] [--budget USD] [--app A] | make a schedule |
syndai schedule edit <id> [--prompt P] [--every …] [--at …] [--tz Z] [--budget USD|none] | change only the fields you pass; --budget none removes the cap |
syndai schedule pause <id> | stop it from running until you resume it |
syndai schedule resume <id> | start it again |
syndai schedule run-now <id> | run it once now; the schedule stays as it is |
syndai schedule delete <id> | delete it; asks y/N in a terminal, and needs --yes in a script |
syndai repeat <run> --every … [--at …] [--tz Z] [--budget USD] | repeat a run you liked on the same project |
When it runs
--every | --at | example |
|---|---|---|
hourly | :MM | --every hourly --at :15 |
daily | HH:MM | --every daily --at 09:00 |
weekdays | HH:MM | --every weekdays --at 17:30 |
weekly | "<day> HH:MM", day is sun to sat | --every weekly --at "fri 16:00" |
| a 5-field cron | none | --every "0 9 1 * *" |
Leave out --at and it picks the next quarter hour. --tz takes a time zone such as Europe/Berlin. It defaults to your computer's time zone, and the CLI always prints the zone it used. Anything else in --every or --at exits 2.
--budget caps what one run may spend, in US dollars.
Repeat a run
syndai repeat $RUN --every weekly --at "mon 09:00"
syndai repeat makes a schedule from a run that went well. Syndai copies that run's full prompt and project on its side, so nothing is cut. It does not start a run right away. The first one happens at the next scheduled time, which the CLI prints.
Safe to run twice
schedule add and repeat send a request id. If the reply is lost and the CLI tries again, you get the schedule it already made (exit 0), not a second one. schedule run-now on a run that is already going also exits 0.
To change what happens when a run overlaps the last one, use the project's Runs tab or syndai api updateSchedule.
Choosing the project
Every command that acts on a project picks it in this order:
--app <project>: an id, the slug, the live host or URL, or the repository name;- the
SYNDAI_APPenvironment variable; syndai link <project>, run in this directory or a parent;- a git remote of the current checkout that matches a connected repository;
- the only project in the workspace;
- a picker, in a terminal.
Otherwise the command exits 2 and lists the projects. syndai link stores its map in $XDG_CONFIG_HOME/syndai/links.json, never in the repository.
Status and settings
syndai status [--app A] lists what is running and every change that needs you. Each one comes with the command that moves it on.
syndai settings [--app A] shows a project's settings. syndai settings set key=value... changes them:
| key | values |
|---|---|
autonomy | merge (go live when every check passes), auto (open a pull request), gated |
feature_requests | off, members, public |
bug_scan | on, off (the weekly Bug Finder) |
protected_paths | protected_paths+=<pattern> adds one; protected_paths=<a>,<b> sets the list |
From the CLI, protected files can only be tightened. Removing a pattern, or adding a ! exemption, needs the owner in the browser (exit 3 with the URL).
Any API route
syndai api calls any route in the public API with your key, like gh api. It reads the list of routes from api.syndai.ai/openapi.json and keeps it for a day. So a new route works the day it ships.
| command | does |
|---|---|
syndai api | list every operation, grouped |
syndai api <operationId> --help | the method, path and summary, and each field (required ones are marked) |
syndai api <operationId> [fields] | call it by name, such as listSchedules |
syndai api <METHOD> <path> [fields] | call a raw path; a path without /api/ is under /api/v1/external/coding |
| option | does |
|---|---|
-f key=value | a text field; repeatable |
-F key=value | a typed field: true, false, null and numbers keep their type, and key=@file reads the value from a file; repeatable |
--input <file> | the whole request body, from a file, or from stdin with -; not mixed with body fields |
--paginate | for a GET, follow next_cursor page by page and print one list (up to 50 pages) |
--json | print JSON |
A field named like a part of the path fills that part. Any other field goes in the query for GET and DELETE, and in the body for the rest. The CLI fills in customer_id, coding_repository_id and repository_id when the path needs them. It never adds them to a query or body.
syndai api listSchedules -f limit=5 --paginate
syndai api getRun -f run_id=$RUN
syndai api createWebhookEndpoint --input hook.json
syndai api GET runs?limit=5
An unknown name prints the three closest names and exits 2. If the call needs a scope your key lacks, see Keys and scopes.
Output and scripting
Progress goes to stderr. The result goes to stdout. The result is JSON when you pass --json, or when stdout is not a terminal (a pipe, CI, a coding agent). Colour is off when stdout is not a terminal or NO_COLOR is set.
For a change, the JSON result is its ChangeState, plus next when there is a command to run next:
| field | meaning |
|---|---|
run_id, project | the change and its project (id, name, live_url) |
state | queued, working, needs_input, landing, held, ready, live, live_unverified, failed, reverted, cancelled |
headline | one line about what is happening |
needs_input | open questions: hitl_request_id, summary, inputs |
held | reason, message, paths, head_sha, can_retry, review_url |
live_url, verified | where it is live, and whether the live check confirmed it |
run_url, receipt_url | the change in the portal, and its public receipt |
done | true once nothing more will happen without you |
cursor | pass to GET /runs/{id}/change?cursor= to wait for the next change |
An error prints error: <one sentence> and next: <command> on stderr. With JSON output it also prints {"error": {"exit_code", "code", "message", "next"}} on stdout.
Error codes
Every refused call has a code. The ones you will meet most:
| code | what to do |
|---|---|
INVALID_SCOPE | the key lacks a scope; the error names it in required_scope (see Keys and scopes) |
HUMAN_APPROVAL_REQUIRED | a person must decide at the review_url (exit 3) |
REPO_NOT_BOUND | the project has no connected repository (exit 2) |
CLI_UPGRADE_REQUIRED | run npx -y syndai@latest |
Exit codes
| code | meaning |
|---|---|
| 0 | done: live and confirmed by the live check, ready for review, or the command succeeded |
| 1 | the change failed, was reverted or cancelled, or the request was refused |
| 2 | a usage error, or no project could be chosen |
| 3 | needs you: a question, a hold, or a decision only the browser can make |
| 4 | live, but the live check could not confirm it |
| 5 | sign-in, scope or plan (401, 402, 403) |
| 6 | --timeout reached; the change keeps going |
| 7 | Syndai did not answer after retries |
| 130 | stopped with Ctrl-C; the change keeps going |
Retries
The CLI retries only reads, and requests that carry their own request id (change, answers, text edits, revert, new schedules). A 429 waits out Retry-After. A 5xx or network error backs off from 0.5 s to 8 s with jitter. After five tries it exits 7. Every request sends User-Agent: syndai-cli/<version>. A CLI older than the server supports gets CLI_UPGRADE_REQUIRED. Run npx -y syndai@latest to fix it.
Files and environment
$XDG_CONFIG_HOME/syndai/credentials.json | keys by workspace, mode 0600 (default ~/.config) |
$XDG_CONFIG_HOME/syndai/links.json | directory to project |
$XDG_STATE_HOME/syndai/pending/ | request ids of changes not yet confirmed (default ~/.local/state) |
$XDG_CACHE_HOME/syndai/ | the API route list, kept for a day (default ~/.cache) |
SYNDAI_API_KEY | the key to use; wins over syndai login |
SYNDAI_APP | the project to use |
SYNDAI_API_URL | the API base (default https://api.syndai.ai) |
To let Claude Code, Cursor or Codex change your projects directly, see MCP.