Templates: saving a body and sending it again
A template is a saved request body plus the parameters that change between sends. It is a JSON file — nothing about it is a database — and fft template render prints the finished body on stdout, so it composes with every command that takes --file:
fft template render rush-order --set email=a@b.de | fft order create --file -Rendering makes no request. It needs no project, no credentials and no network, and a read-only project cannot refuse it: the command on the right of the pipe is still gated exactly as it always was. That is the whole design — templates add a way to build a body, not a new way to reach the tenant.
Saving one
fft template save rush-order --file body.json --description "Rush order for the flagship"
fft order get ORDER-1 -o json | fft template save from-order --file -
fft template save facility --from addFacility
fft template save rush-order --operation addOrder --file body.json--from <operationId>seeds the body from the spec's own example and records which operation it is for, which is the fastest way to start one from nothing.--operation <operationId>records the operation a--fileor--databody is for. The id must be onefft api listshows, and one that takes a body; anything else is exit 2 and nothing is written. It cannot be combined with--from, which names one already.- A top-level
versionis dropped on the way in, and fft says so. A version is only true of the entity at the moment it was read; replaying a saved one is a guaranteed 409. Put one back at render time with--set version=Nif you really mean to. --localwrites./.fft/templates, which is meant to be committed and shared. Without it templates go to$XDG_DATA_HOME/fft/templates, which is yours alone. A project template of the same name wins; saving a personal one it hides warns, and-o jsonnames the project file undershadowedBy.- A body bound for
--localthat looks like it carries a credential (a key name matchingpassword,secret,apikey,tokenorauthorization) is refused outright unless you confirm — or pass--yeson a script's non-interactive terminal.
Declaring parameters
--param gives a path a short name; --require makes it mandatory:
fft template save rush-order --file body.json --require email=order.consumer.email --param qty=order.orderLineItems.0.quantity=1fft template show rush-order then prints what the template wants before you type a --set, and rendering without a required parameter fails with exit 2 naming all of the missing ones at once.
A parameter name is what --set name=value addresses, so it cannot hold a ., a \ or an =, start with a dash, or start or end with white space. A template file that declares such a name — a hand-edited or committed one — is refused when it is read, and fft template list names it on stderr.
Changing values
--set takes either a declared parameter or a path into the body, and the two are the same namespace — a declared name can never contain a dot, so there is nothing to disambiguate:
fft template render rush-order --set email=a@b.de --set order.orderLineItems.0.quantity=3
fft template render rush-order --set email=a@b.de --set order.orderLineItems.1.quantity=1- A missing object on the way is created.
--set order.address.city=Berlinworks against a body with noaddress. - A numeric segment indexes an array, and one index past the end appends — which is how you build a list up one
--setat a time. Further past the end is exit 2 naming the length, not a body padded with nulls the API will reject with an opaque 400. - A path that would bury a scalar (
--set name.first=xwherenameis a string) is exit 2. fft will not silently delete a field you still have. - A value that parses as JSON is that JSON:
3is a number,truea boolean,nulla null,{"a":1}an object.
Ids made only of digits need --set-string. --set tenantOrderId=12345 sends the number 12345; the API wants the string "12345". This is the same trap as everywhere else in fft — see Ids are not numbers — arriving from the other direction:
fft template render rush-order --set-string order.tenantOrderId=12345Rendering what you reviewed
fft template show prints a DIGEST for the template as it read it. render --if-digest renders only while the template still has that digest, and otherwise exits 7 with nothing on stdout — so a script, or a person, that read a template before sending it sends that template, not whatever a git pull has made of the file since:
fft template render rush-order --if-digest 3f5a… --set email=a@b.de | fft order create --file -The digest is the SHA-256 of the template's own encoding, the fields show -o json prints, so it does not move when the file is merely re-indented.
show -o json also prints a resolved object — name, scope (project or user) and path — naming the file it read. A project template hides a user template of the same name, so read the scope from there rather than from an earlier template list. The key is not part of the template: piping the document into template save --file - ignores it.
Tenants do not share ids
A template records the project it was saved under. Rendering it while another project is active still works — promoting a body from staging to production is a real thing people do — but fft warns on stderr, because facility ids, consumer ids and order ids are exactly what does not survive the move.
Read a project template before you commit it. A body captured from real work carries real ids and real consumer emails, and git history is not something you can quietly edit later.
The commands
fft template list
fft template show rush-order
fft template render rush-order --set email=a@b.de
fft template save rush-order --file body.json
fft template remove rush-orderfft template listprints one row per name — exactly the setrenderwould resolve. A personal template hidden by a project one of the same name is reported on stderr instead.fft template renderalways prints JSON, even under-o yaml: the body is going into a command that reads JSON.- Exit 6 is a template nobody saved; the error names the ones that exist.