The curated commands
Hand-written commands for the entities most people touch, plus the core commands that have nothing to do with the API. Everything else lives in discovery.md.
Run --help on any of them. It tells you the endpoint, the permission it needs, and a sample body. The notes below are the things --help will not tell you.
Orders
An order is the demand the platform fulfills, addressed by its platform id (the one list, search and get print — orders have no tenantFacilityId-style shorthand).
fft order list --consumer-id C-4711
fft order get 7f3ce2a4-1f00-4c6a-9a2b-5d1f0b7a1c33
fft order search --status OPEN --sort orderDate:desc
fft order search --since 2026-07-01 --until 2026-07-15
fft order create --example > order.json
fft order create --file order.json
fft order update 7f3ce2a4-1f00-4c6a-9a2b-5d1f0b7a1c33 --file order.json
fft order cancel 7f3ce2a4-1f00-4c6a-9a2b-5d1f0b7a1c33 --reason-id out-of-stock
fft order unlock 7f3ce2a4-1f00-4c6a-9a2b-5d1f0b7a1c33listis the plain GET: it returns a stripped order (id, status, orderDate, line count, version) and filters only on--tenant-order-idand--consumer-id. For status, a date range or a sort, usesearch.searchis a BETA endpoint guarded byADMIN_MODULES_READ, notORDER_READ— a token that lists orders fine may be refused here. Give it a narrow--since/--until; it pages far faster when the date range is bounded.- Neither
listnorsearchfilters by facility. An order routes to a facility through its pickjobs, so filter pickjobs by facility instead. updateis a PATCH, butorderLineItemsis a full replacement: send them all or the ones you omit are deleted. Read the order, edit, send it back.cancel --forcesends FORCE_CANCEL, which only works if the tenant permits it; it takes no--reason-id. Cancelling cannot be undone, so fft asks first (--yesanswers).
Facilities
A facility is addressed by its id.
fft facility list --status ACTIVE
fft facility get berlin-warehouse
fft facility create --example > facility.json
fft facility create --file facility.json
fft facility patch berlin-warehouse --name "Berlin Warehouse"
fft facility delete berlin-warehouse
fft facility coordinates set berlin-warehouse --lat 52.52 --lon 13.405
fft facility search --file query.jsonpatchchanges named fields;updatereplaces the whole object from a file. Preferpatch— it cannot silently drop a field you did not know about.deleteis destructive and irreversible. Ask first, every time.
Connections
A connection is an edge of the fulfillment graph: an outbound lane from one facility to a SUPPLIER, to another MANAGED_FACILITY, or to the CUSTOMER. The router can only source along an edge that exists, so this is the first place to look when an order routes somewhere surprising — or refuses to route at all.
A connection belongs to the facility it leaves, so every command needs --facility as well as the connection's own id:
fft connection list --facility berlin-warehouse
fft connection list --facility berlin-warehouse --target frankfurt-warehouse
fft connection get 3f9c1e77-2b4a-4f0e-9d61-8a2c5b7e4d10 --facility berlin-warehouse
fft connection create --facility berlin-warehouse --example --type SUPPLIER
fft connection delete 3f9c1e77-2b4a-4f0e-9d61-8a2c5b7e4d10 --facility berlin-warehouseupdateis a PUT and there is nopatch. The connection becomes what the file says and loses anything the file omits — so unlike a facility, you cannot reach for the safer verb. Read the whole thing, edit it, send it back. fft supplies theversion.- The connection the API returns carries its type only inside
target; the body it accepts wants one at the top level too. fft fills that in, sofft connection get … -o jsonpiped back into--fileworks. Do not hand-edit it in. --exampleprints a body per type:--type SUPPLIER,MANAGED_FACILITYorCUSTOMER. ACUSTOMERtarget names no facility, and is not missing one.deleteremoves a routing edge. Orders stop being sourced along it immediately, and if it was the only way to reach a target, that target becomes unreachable. Ask first.
Sourcing
fft sourcing simulate changes nothing. It is a POST, and it is a read: it runs the router against a hypothetical order and hands back the ways it could be fulfilled. No order is created, no stock is reserved, nothing moves. It works on a read-only project, and fft knows it does — so do not refuse to run it on the grounds that POST means write.
fft sourcing simulate --example > order.json
fft sourcing simulate --file order.json --results 3
fft sourcing get 284f32cc-b106-487d-b633-f90d93d8c251- An empty answer is not "no matches". It means the router could not fulfil the order at all, from anywhere. Say that, rather than reporting that the search came back empty.
- The penalty is a penalty: lower is better. The table is sorted so the router's favourite is first. Never present it as a score.
UNSOURCEDis the partial-failure column. An option can look healthy and still have quietly dropped items. If it is not zero, that option will not fulfil the whole order.--resultsdefaults to 3 because the API's default is 1 — one option is an answer, not a set of options.- Every transfer carries a
facilityConnectionRef. That is the join tofft connection get, and it is how you answer "why there?" — see the recipe in recipes.md.
Routing
A routing strategy is the decision tree the engine walks to source an order: a root node with fences (which facilities are eligible) and ratings (which is preferred), and conditions that branch to further nodes. Only one strategy is ever live — inUse — and fft routing is where you inspect, edit and switch it.
fft routing strategy list
fft routing strategy get 3f9c1e77-2b4a-4f0e-9d61-8a2c5b7e4d10 -o json
fft routing strategy create --example > strategy.json
fft routing strategy activate 3f9c1e77-2b4a-4f0e-9d61-8a2c5b7e4d10
fft routing strategy evaluate 3f9c1e77-2b4a-4f0e-9d61-8a2c5b7e4d10 --file order.jsonactivatemakes one strategy live, taking over from whichever wasinUse. It is versioned in the body, so a stale version is exit 7 — re-read and retry.--if-versionskips the read for a clean 409.fft routing strategy actions <id> --fileis the escape hatch for the other four actions (COPY,REPLACE_NODE,REPLACE_CONDITION,REPLACE_GLOBAL_CONFIGURATION) —REPLACE_NODE/REPLACE_CONDITIONedit one part of the tree by id, the safer alternative to a whole-strategy PUT.evaluateis a read. It dry-runs the strategy against an order (the same body shapefft order createtakes) and reports the path the engine would walk. It reserves nothing and is safe under--read-only. This answers "why would this strategy route this order there?";fft sourcing simulateanswers the same question for the live one.fft routing strategy evaluate-node <id> <nodeId>evaluates a single node instead of the whole tree.updateis a PUT, there is nopatch. Read the whole strategy, edit it, send it back; fft supplies theversion. There is nodelete— the API does not offer one.fft routing decision-logsis the audit trail of real routing runs. Filter by exact id:--order,--routing-plan,--process,--tenant-order-id,--sourcing-option(s).fft routing categorymanages the node config category labels (a name and a colour) that a strategy's nodes are grouped under. Full CRUD, includingdelete.
Listings
A listing has no id of its own. It is addressed by the facility it is in and the article it is for, so every command that addresses a single listing needs --facility (the bulk upsert is the exception: it reads the facility from the file):
fft listing list --facility berlin-warehouse
fft listing get --facility berlin-warehouse ARTICLE-123
fft listing patch --facility berlin-warehouse ARTICLE-123 --status ACTIVE
fft listing delete --facility berlin-warehouse ARTICLE-123fft listing set --facility <id> --file listings.jsonis a PUT, and all or nothing: one bad listing rejects the whole file. But it is not a replacement of the catalog — listings the file does not mention are left alone. You do not have to send a catalog back to keep it.fft listing upsert --file listings.jsonis the bulk path, and the one you almost always want. It is chunked, and it reports per item — so it can exit 8 with some items written, wheresetwould have written none.fft listing purge --facility <id>deletes every listing in a facility. There is no narrower way to say "I meant that".
Stock
Stock has an id of its own, but you will nearly always arrive at it through the facility and the article.
fft stock list --facility berlin-warehouse --tenant-article-id ARTICLE-123
fft stock get 7f3ce2a4-1f00-4c6a-9a2b-5d1f0b7a1c33
fft stock summary --facility berlin-warehouse
fft stock upsert --file stocks.json
fft stock actions --file action.jsonstock summaryanswers "how much of this do we have" without paging every stock record. Reach for it before you reach forlist.stock actionsis how stock is moved and changed (pick, book in, correct) rather than overwritten. If the user is describing a real-world event, this is usually the endpoint.stock upsertis bulk and chunked; exit 8 means some items landed.
Templates
A saved request body with parameters. render prints the finished body on stdout, so it composes with any command that takes --file:
fft template save rush-order --file body.json --require email=order.consumer.email
fft template save rush-order --operation addOrder --file body.json
fft template list
fft template show rush-order
fft template render rush-order --set email=a@b.de | fft order create --file -
fft template remove rush-order- Rendering makes no request: no project, no credentials, no network, and a read-only project cannot refuse it. The command receiving the body is still gated as it was.
--settakes a declared parameter or a path (--set order.items.0.quantity=3). An id made only of digits needs--set-string, or it goes out as a number.- Saving strips a top-level
version, because replaying a stale one is a guaranteed 409. --operation <operationId>records which operation the body is for; an unknown id, or one that takes no body, is exit 2.render --if-digest <digest>renders only the template whose DIGESTshowprinted, and exits 7 if the file changed since — for a script that reviews a template before it sends it.show -o jsonnames the file it read underresolved(name,scope,path): a project template hides a user one of the same name.--localwrites./.fft/templates, which the repository commits. Read one before you commit it: a body captured from real work carries real ids and consumer emails.- Full rules in templates.md.
Paging
Every list and search takes the same flags:
fft stock list --facility berlin-warehouse --size 100
fft stock list --facility berlin-warehouse --all --max-items 500
fft facility list --total- The default is one page.
--allpages to the end, stopping at--max-items— which defaults to 10,000, so--allis bounded whether you say so or not. Lower it when the question is exploratory; a real tenant is a great many requests. - The API has two paging models and they have different page sizes. The searches behind
facility,listingandstockdefault to 20 per page;fft connection listis a plain GET and defaults to 25.--sizeoverrides either. Do not assume a page size — count the rows, or pass--size. - Hitting that limit is not an error: fft warns on stderr and exits 0 with what it got, so the JSON alone cannot tell you the answer was cut short.
--totalprints the count to stderr, never into the JSON. Only on a single page does it ask the API to count: under--allfft prints the number it actually fetched, and — if the run was truncated — prints nothing at all, because then it cannot know the total.
Core commands
fft project list
fft project current
fft project use staging
fft project read-only prod
fft auth whoami
fft auth status -o json
fft ping
fft version
fft skill status -o json
fft component list
fft history top -o json
fft history top --all-projects --limit 0 -o json
fft history list --limit 10 -o json
fft history list --project staging -o json
fft history list --all-projects -o json
# only when the user asks; it asks first
fft history clearfft auth whoamiprints the permissions the current credentials actually have. When something exits 5, this is the command that explains why.fft auth status -o jsonsays what is stored to sign in as the currentproject(with itsemailandusername) —store,signIn(password,idTokenornone),hasPassword,hasRefreshToken,hasIdToken— and the cached token'stokenstate (valid,expiring,expired,unknown,none) with itsexpiresAtandexpiresIn. It takes no flags of its own, and exits 3 when no project is configured. It sends nothing and mints nothing, so it is the cheap check before a long run;expired: trueis not an error, the next command renews the token. It never prints a credential.fft pingneeds no credentials at all. It is the way to tell "the tenant is down" apart from "my token is wrong".fft project read-only prodmarks a project read-only for good, in the config file. It is the right suggestion whenever a user tells you a project is production.fft skill status -o jsoncompares each installed copy of this skill with the running fft:{fft, installs: [{scope, dir, version, status, error}]}, wherestatusisCURRENT,OUTDATED(installed by another fft version),MODIFIED(edited since),MISSING,NOT_SKILLorUNREADABLE(with the reason inerror).fft skill installupdates anOUTDATEDcopy without--forceunless the user edited a file in it; then it stops and asks. Status only reads the installed files, sends nothing, and always exits 0.fft component listsays which components are installed, and whether fft ships them or somebody else does — a command in--helpthat came from a component is somebody else's code. See components.fft history top -o jsonlists the operations used most in the current project, each withproject,operationId,command,countandlastUsed— a quick way to learn what a user normally does before suggesting a command.--all-projectscounts every project, each separately;--limit(default 10,0for all) caps the list. It needs a current project unless--all-projectsis given: exit 3 otherwise.fft history list -o jsonprints the recent requests in the current project, newest first;--all-projectslists every project's;--limitdefaults to 20,0for all. Liketop, it needs a current project unless--all-projectsis given: exit 3 otherwise. Each entry hasv(the record's format version),ts,source(cliortui),project,operationId,command,args,status(left out when no response arrived),exitanddurationMs.- Neither holds a request body, and inline
--data, header values,--setvalues, credential-shaped flags and flags or query parameters named like a person's data (--email,--query email=…,--search-term) appear as<redacted>; ids and other values are kept as typed. History is off in headless mode unlessFFT_HISTORY=on, so in CI both usually print[]; when nothing is being recorded, stderr says why. fft history cleardeletes the whole history. It asks first, and without a terminal it refuses with exit 2 unless--yesis given. It is the user's record: do not clear it unless they ask.fft component init pricingscaffolds a new component (a command or a transport, in shell, Go, Python or Node) that installs and runs straight away. See components.