Operations
Every change to an app goes through an operation: it checks the change, edits the spec files, regenerates the code, migrates the database and commits a checkpoint, so smithable undo takes it back. This page is generated from the operations themselves (100 of them), so it matches the Smithable it was built with. Run one with smithable op <name> …; smithable op <name> --help shows the same, and smithable mcp offers them as tools to AI agents. Change an app without writing code walks through five of them.
Models and fields
setNamedBy
Say what names a record of a model, its title wherever one record is shown or referred to: one part or several (a field, a reference for that record's own name, or a field one reference away like classType.name), joined with " · "; --none leaves it to the rule again (the first short text, else the first long text).
smithable op setNamedBy <model> [parts] [--field …] [--none …]
<model>(a model's name)<parts>(a list, optional): The parts in order, e.g. classType startsAt, or one: company, or session.--field(a field's name, optional): One part: the same as parts with one item.--none(true or false, optional): true removes the setting: the rule picks the title again.
Never loses data.
setThread
Say which fields a conversation reads from a model's records: the message's body (a text field), its author (a person or email field) and the side that is ours (a two-valued choice and its value); --none leaves it to the rule again.
smithable op setThread <model> [--body …] [--author …] [--side …] [--ours …] [--none …]
<model>(a model's name)--body(text, optional): The text, markdown or string field shown as the message.--author(text, optional): The field that says who wrote it: a person, or an email.--side(text, optional): The two-valued choice field that says which side a message is on.--ours(text, optional): The side field's value that is our side; needed with --side.--none(true or false, optional): true removes the setting: the rule reads the fields again.
Never loses data.
Example: smithable op setThread Message --body content --author sender --side sender --ours agent
setUnique
Make two or more fields of a model unique together: no two records hold the same values in all of them, among one owner's records (owner), in the whole app (app), or as the access says; --remove takes the rule away. One field alone is unique through setFieldRules.
smithable op setUnique <model> <fields> [--scope …] [--remove …]
<model>(a model's name)<fields>(a list): Two or more of the model's stored fields.--scope(ownerorapp, optional): owner: among one owner's records; app: in the whole app. Left out: as the access says, like a field's unique: true.--remove(true or false, optional): true removes the rule for these fields.
Never loses data.
addModel
Add a model with its fields.
smithable op addModel <model> <fields>
<model>(text): PascalCase name, e.g. Invoice.<fields>(key=value pairs)
Never loses data.
addField
Add a field to a model.
smithable op addField <model> <field> <type> [--formula …] [--min …] [--max …] [--default …] [--list …]
<model>(a model's name)<field>(text): camelCase name, e.g. phone.<type>(text): Field shorthand, e.g. "phone", "string!", "draft | sent".--formula(text, optional): A computed field: worked out from the record's other fields, never entered, e.g. "quantity * unitPrice" (the type is then its kind only, e.g. "money").--min(a number, optional): The least value (int, decimal, money) or the shortest length (string, text).--max(a number, optional): The greatest value (int, decimal, money) or the longest length (string, text).--default(text, optional): With "min" or "max": the default, as its own value (never "=" in "type").--list(true or false, optional): Also show it as a column of its list page's table, in the same change.
Never loses data.
Example: smithable op addField Incident severity "minor | major | critical"
changeField
Change a field's type, required flag, uniqueness or default.
smithable op changeField <model> <field> <type> [--renames …]
<model>(a model's name)<field>(a field's name)<type>(text): Field shorthand, e.g. "email! unique".--renames(a list, optional): For a choice field: what records holding a value that goes get instead, as "old=new" (e.g. doing=inProgress); "old=" leaves them empty.
Can lose data in some cases (removing or narrowing what holds records); then it asks first, or takes --yes.
Example: smithable op changeField Order status "draft | placed | onItsWay | canceled" --renames shipped=onItsWay
setFieldRules
Set who fills a field and when it may change: by app (the form never shows it), locked (after it is made, or while a condition holds), required while a condition holds; and whether it is unique, and where (owner or app).
smithable op setFieldRules <model> <field> [--by …] [--locked …] [--once …] [--required …] [--unique …]
<model>(a model's name)<field>(a field's name)--by(appor ``, optional): app: the form never shows it; its default, an action or code fills it. "" removes it.--locked(key=value pairs, optional): Read-only while the record fits the condition, e.g. { status: closed }; {} removes it.--once(true or false, optional): Locked once made: set when the record is made, never changed after; false removes it.--required(key=value pairs, optional): Required while the record fits the condition, e.g. { status: "inProgress | closed" }; {} removes it. Always required is "!" in the type (changeField).--unique(trueorfalseorownerorapp, optional): No two records hold the same value: true (per owner or in the app as the access says), owner (among one owner's records), app (in the whole app); false removes it.
Never loses data.
Example: smithable op setFieldRules Order total --locked status=shipped
renameField
Rename a field, keeping its data.
smithable op renameField <model> <from> <to>
<model>(a model's name)<from>(a field's name)<to>(text)
Never loses data.
removeField
Remove a field and its data.
smithable op removeField <model> <field>
<model>(a model's name)<field>(a field's name)
Can lose data in some cases (removing or narrowing what holds records); then it asks first, or takes --yes.
renameModel
Rename a kind of record, keeping its records. Every field, page, action, rule and grant that names it follows.
smithable op renameModel <from> <to>
<from>(a model's name)<to>(text): The new name, like Customer.
Never loses data.
setCapacity
Limit how many records of a model each referenced record may have, e.g. a session's bookings up to its capacity ("none" as per removes the limit). The capacity is set whole: a where or a deadline left out is lifted.
smithable op setCapacity <model> <per> [max] [where] [--closes …] [--cancelUntil …]
<model>(a model's name)<per>(a field's name): A required reference of the model, e.g. session; "none" removes the capacity.<max>(text, optional): A whole number, or a whole-number field of the referenced record, e.g. "session.capacity".<where>(key=value pairs, optional): Only records with these values count, e.g. { "status": "confirmed" } or { "status": "confirmed | paid" }.--closes(text, optional): Until when a record may be added: a date-time of the referenced record ("session.startsAt"), as it is or a period before it ("2 hours before session.startsAt"); "none" lifts it.--cancelUntil(text, optional): Until when a record's owner may delete it ("24 hours before session.startsAt"); roles that delete everyone's pass; "none" lifts it.
Never loses data.
setSchedule
Say a model's records are booked by their time (barbers, rooms): weekly hours, breaks, minutes between bookings, and a model of time away ("none" as hours removes it).
smithable op setSchedule <model> <hours> [--breaks …] [--gap …] [--away …]
<model>(a model's name)<hours>(text): The weekly hours, e.g. "mon-fri 09:00-17:00, sat 10:00-14:00" or { "mon-fri": "09:00-17:00" }; "none" removes the schedule.--breaks(text, optional): Kept free every open day, e.g. "12:00-12:30".--gap(text, optional): Minutes kept free after each booking.--away(text, optional): A model whose records block time, e.g. TimeOff: its one reference to this model and its first two date-and-time fields; or { "model": "TimeOff", "resource": "barber", "from": "from", "until": "until" } to say them.
Never loses data.
setOccupies
Say each record of a model takes a scheduled record's time, e.g. an appointment its barber's, from its start for its service's minutes ("none" as resource removes it).
smithable op setOccupies <model> <resource> [starts] [duration] [while] [--cancelUntil …] [--cancelTo …]
<model>(a model's name)<resource>(a field's name): A required reference to a model with a schedule, e.g. barber; "none" removes it.<starts>(a field's name, optional)<duration>(text, optional): Minutes: a whole-number field, or one of a referenced record, e.g. "service.minutes".<while>(key=value pairs, optional): Only records with these values take the time, e.g. { "status": "requested | confirmed" }.--cancelUntil(text, optional): How long before the start its owner may cancel it: "30m", "12h" or "2d".--cancelTo(text, optional): The value the owner's Cancel sets, when several are outside while.
Never loses data.
setTrash
Keep a model's deleted records in a trash for thirty days, where whoever may delete them can restore them (true), or delete for good at once (false, the default).
smithable op setTrash <model> <trash>
<model>(a model's name)<trash>(true or false): true: a trash with restore; false: none.
Can lose data in some cases (removing or narrowing what holds records); then it asks first, or takes --yes.
setReview
Hold a model's pictures and files for review before others see them: the fields named, "all" (every picture and file field), or "none". A required one holds the whole record until approved, an optional one only the file; admins and the roles granted review approve them on a review page.
smithable op setReview <model> <fields>
<model>(a model's name)<fields>(a field's name): Picture and file fields, ["all"] for every one, or ["none"].
Never loses data.
Pages and navigation
renamePage
Rename a page. Its menus, grants, texts and look follow; without a route of its own, its address becomes / plus the new name.
smithable op renamePage <from> <to>
<from>(a page's name)<to>(text): The new name, like invoices.
Never loses data.
removePage
Remove a page, from the menus too. Its records and their data stay.
smithable op removePage <page>
<page>(a page's name)
Never loses data.
addPage
Add a page that lists, shows, creates, edits and deletes a model, and add it to the main menu.
smithable op addPage <page> <model> [route]
<page>(text): Page name, e.g. invoices.<model>(a model's name)<route>(text, optional): Defaults to / plus the page name.
Never loses data.
setSearch
Let a page's list be searched by these fields, or remove the search (no fields).
smithable op setSearch <page> [fields]
<page>(a page's name)<fields>(a field's name, optional): Text, email, phone, web address or choice fields of the page's model.
Never loses data.
setFilters
Let a page's list be filtered by these fields (choices, references, numbers, dates, yes/no), or remove the filters (no fields).
smithable op setFilters <page> [fields]
<page>(a page's name)<fields>(a field's name, optional): Choice, reference, number, amount, date or yes/no fields of the page's model.
Never loses data.
setDefaultFilter
What a page's list is filtered by until the viewer chooses: a filter and its value, as the address carries it (startsAt upcoming; status "open,pending"); "none" takes the default away.
smithable op setDefaultFilter <page> <field> <value>
<page>(a page's name)<field>(a field's name): One of the page's filters.<value>(text): A date filter: upcoming, past, today, thisWeek, thisMonth or a range (2026-01-01..); a number: a range (10..500); a choice: its values, comma-separated; yes/no: true or false; owner: mine; a person: me or none. "none" removes the default.
Never loses data.
Example: smithable op setDefaultFilter sessions startsAt upcoming
setSortable
Let a page's list be sorted by these fields from a Sort by control, or remove the choice (no fields).
smithable op setSortable <page> [fields]
<page>(a page's name)<fields>(a field's name, optional): Stored fields of the page's model with an order: not pictures or long text.
Never loses data.
setFavorites
Let people save a page's records (a heart on each, and a Saved view), list only the viewer's saved ones (only), or remove favorites (off).
smithable op setFavorites <page> [mode]
<page>(a page's name)<mode>(onoronlyoroff, optional): on: a heart and a Saved view (default); only: the viewer's saved records; off: none.
Never loses data.
setContact
Give a page's record page a contact block that mails the record's owner (or the user a field names), optionally keeping a copy in a model; no recipient removes it.
smithable op setContact <page> [to] [--keep …]
<page>(a page's name)<to>(a field's name, optional): owner, or a user field of the page's model, e.g. seller.--keep(a model's name, optional): A model of its own for messages, with no links to other records.
Never loses data.
setMine
Make a page list only the viewer's own records (my items, my orders), or every record again (off).
smithable op setMine <page> [mode]
<page>(a page's name)<mode>(onoroff, optional): on (default) or off.
Never loses data.
addLandingPage
Add a landing page for visitors, starting with a hero and a footer (add more with addSection). It opens without sign-in unless public is false.
smithable op addLandingPage <page> [route] [--public …]
<page>(text): Page name, e.g. home.<route>(text, optional): Defaults to / when free, else / plus the page name.--public(true or false, optional): Open without sign-in (the default).
Never loses data.
Example: smithable op addLandingPage home
addSection
Add a section to a page, below its others: one named after its kind (hero, features, pricing, testimonials, faq, callToAction, contact, gallery, footer), or any name with records (a model's records as cards, a table or a calendar), text (a heading and paragraphs) or custom. Its texts go in smithable.content.yml.
smithable op addSection <page> <section> [--records …] [--view …] [--starts …] [--text …] [--kind …] [--where …] [--form …] [--button …] [--custom …]
<page>(a page's name): The page, e.g. home or about.<section>(text): A kind (hero, features, pricing, testimonials, faq, callToAction, contact, gallery, footer) or, with records, text or custom, any name.--records(a model's name, optional): A model whose records the section shows.--view(cardsortableorcalendar, optional): With records: as cards (the default), a table or a calendar.--starts(text, optional): A calendar view: the date field the records are placed by.--text(true or false, optional): A text section, any name: a heading and paragraphs from smithable.content.yml.--kind(heroorfeaturesorpricingortestimonialsorfaqorcallToActionorcontactorgalleryorfooter, optional): A section of this kind under another name, e.g. a second faq called more.--where(key=value pairs, optional): With records: only those with these values, e.g. { "status": "investigating | identified" } or { "featured": "true" }.--form(a model's name, optional): A contact section's form: the model visitors add.--button(a page's name, optional): The page a hero's or call to action's button opens.--custom(text, optional): A section of your own, any name: its component in src/custom/sections/, e.g. RepaymentChart.
Never loses data.
removeSection
Remove a section from a page, with its texts. A page kind's own part (a crud page's list) stays.
smithable op removeSection <page> <section>
<page>(a page's name)<section>(a landing section)
Never loses data.
moveSection
Move a page's section above or below another of its sections: they show in the order written. A page kind's own part moves once its name is among the sections.
smithable op moveSection <page> <section> [--before …] [--after …]
<page>(a page's name)<section>(a landing section)--before(a landing section, optional): The section it goes above (one of before and after).--after(a landing section, optional): The section it goes below (one of before and after).
Never loses data.
setSectionWhere
Show only some of a records section's records: those with these values (none shows every record).
smithable op setSectionWhere <page> <section> [where]
<page>(a page's name)<section>(a landing section)<where>(key=value pairs, optional): Field values a record must have, e.g. { "status": "investigating | identified" } or { "featured": "true" }; none shows every record.
Never loses data.
Example: smithable op setSectionWhere home open "status=investigating | identified | monitoring" "severity=major | critical"
setShortcuts
Give a landing page's hero up to four shortcuts to other pages, shown as round icons (no pages removes them).
smithable op setShortcuts <page> [pages]
<page>(a page's name): e.g. home.<pages>(a page's name, optional): Up to four pages; their titles and icons are the shortcuts'.
Never loses data.
addSectionsPage
Add a page of parts and no kind (an about page), starting with a text section; add more with addSection. It is in the main menu.
smithable op addSectionsPage <page> [route]
<page>(text): Page name, e.g. about.<route>(text, optional): Defaults to / plus the page name.
Never loses data.
addTile
Add a tile to a dashboard page (created, and added to the main menu, when it does not exist): a count, sum or average of records, a list of the latest ones, or a custom query's records.
smithable op addTile <page> <tile> <measure> <of> [--where …] [--by …] [--limit …]
<page>(a page's name): The dashboard page, e.g. overview.<tile>(text): The tile's name, camelCase, e.g. outstanding.<measure>(countorsumoraverageorlistorquery)<of>(text): A model for count and list ("Invoice"), a number field for sum and average ("Invoice.total"), a query name for query.--where(key=value pairs, optional): Only records with these values, e.g. { "status": "sent" } or { "status": "sent | overdue" }.--by(text, optional): A date field: one bar per month.--limit(a number, optional): For a list or query: how many records.
Never loses data.
addUsersPage
Add a page where administrators invite people by email address with a role, change roles and remove users; it goes in the main menu. An app has one.
smithable op addUsersPage [page] [route]
<page>(text, optional): Page name; team when left out.<route>(text, optional): Defaults to / plus the page name.
Never loses data.
addDataPage
Add an administrators' page with every model's records (or the models named) to find, open, change and delete; it goes in the main menu.
smithable op addDataPage [page] [route] [models]
<page>(text, optional): Page name; admin when left out.<route>(text, optional): Defaults to / plus the page name.<models>(a model's name, optional): Leave out for every model.
Never loses data.
addReviewPage
Add the page where the pictures and files that wait for review (a model's review) are approved or rejected, for admins and the roles granted review; it goes in the main menu.
smithable op addReviewPage [page] [route] [models]
<page>(text, optional): Page name; review when left out.<route>(text, optional): Defaults to / plus the page name.<models>(a model's name, optional): Leave out for every model with review.
Never loses data.
addCalendarPage
Add a page showing a model's records in a week or month calendar, placed by a date or date-time field; it goes in the main menu.
smithable op addCalendarPage <page> <model> [starts]
<page>(text): Page name, e.g. schedule.<model>(a model's name)<starts>(a field's name, optional): Needed when the model has more than one date field.
Never loses data.
addCalculatorPage
Add a calculator page: fields written as a model's, the entered ones its inputs and the formula fields its results, stored nowhere; it goes in the main menu.
smithable op addCalculatorPage <page> <fields>
<page>(text): Page name, e.g. mortgage.<fields>(key=value pairs): Field name → type shorthand, e.g. { "price": "money = 300000", "years": "int = 25" }; a result is a computed field, { "type": "money", "formula": "price / years" }.
Never loses data.
Example: smithable op addCalculatorPage mortgage price=money years=int
addArticlesPage
Add a page showing a model's records as articles (a blog or news): a list, and a page per article; it goes in the main menu.
smithable op addArticlesPage <page> <model>
<page>(text): Page name, e.g. blog.<model>(a model's name)
Never loses data.
addCustomPage
Add a page of your own (a chart, a map, a planner): its component is created once in src/custom/pages/ and is yours; the route, access and menu entry stay Smithable's.
smithable op addCustomPage <page> <component>
<page>(text): Page name, e.g. planner.<component>(text): The component's name, e.g. Planner.
Never loses data.
Example: smithable op addCustomPage planner Planner
addPrivacyPage
Add a privacy page written from the app itself (who runs it, what it stores and why, cookies, mail, logs, rights); open to visitors and linked from the site's footer. The operator's name and contact go in smithable.content.yml.
smithable op addPrivacyPage [page] [route]
<page>(text, optional): Page name; "privacy" unless said otherwise.<route>(text, optional): Defaults to /privacy.
Never loses data.
addOrderPage
Add a page where visitors choose records of one model (dishes) and send them as one order of another (with its lines and a number counting up); it is not in the menu.
smithable op addOrderPage <page> <from> <into> [--ask …] [--route …]
<page>(text): Page name, e.g. menu.<from>(a model's name)<into>(a model's name)--ask(a list, optional): The order's fields the customer fills, in order, e.g. [name, phone, wanted, note].--route(text, optional): Its address, e.g. / for the home page.
Never loses data.
setNavigation
Set which pages a menu shows, in order. The list replaces the menu: include every page that stays.
smithable op setNavigation <menu> <pages>
<menu>(text)<pages>(a page's name)
Never loses data.
Access and sign-in
grant
Set what a role may do beyond the record rule, in smithable.auth.yml: the pages it opens, the models it adds to (create), reads, changes (write) or deletes for everyone, the custom actions it runs, or the models whose waiting pictures and files it approves (review). "all" names everything, "none" nothing; "grant
smithable op grant <role> <what> [names]
<role>(a role)<what>(allornoneorpagesorreadorcreateorwriteordeleteoractionsorreview): pages, read, create, write, delete, actions or review; or all / none for the role as a whole.<names>(a list, optional): The pages, models or actions; ["all"] for every one, none (or ["none"]) for nothing.
Never loses data.
Example: smithable op grant customer actions cancel
setSignUp
Choose who may sign in: anyone with an account at the sign-in provider (within allowedDomains, when set), or only invited people and the admins (needs a users page).
smithable op setSignUp <who>
<who>(anyoneorinvited): anyone: open sign-up; invited: only invited addresses and the admins.
Never loses data.
Example: smithable op setSignUp invited
setDefaultAccess
Choose the record rule: shared (everyone who signs in sees every record) or private (each person sees only their own); everyone changes and deletes their own, and the roles' grants say the rest.
smithable op setDefaultAccess <access>
<access>(sharedorprivate): shared: everyone signed in sees everything; private: only their own records.
Never loses data.
Example: smithable op setDefaultAccess shared
setRoles
Set the app's roles with a description each ("admin" always exists and does everything), and which one new users get. A role keeps what it was granted; a new one starts with the record rule only, so grant it its pages.
smithable op setRoles <roles> [--defaultRole …]
<roles>(key=value pairs): Role name (camelCase) to description, e.g. { "sales": "Works with customers." }.--defaultRole(text, optional): The role new users get; needed with several.
Never loses data.
setPublic
Set what anyone on the internet may use without signing in: pages to open, models to read, models to create (a contact form). Each list replaces the old one; an empty list closes it, and the confirmation says what visitors lose.
smithable op setPublic [--pages …] [--read …] [--create …]
--pages(a page's name, optional): Pages anyone may open.--read(a model's name, optional): Models anyone may read.--create(a model's name, optional): Models anyone may add a record of, e.g. a contact form's.
Never loses data.
setPublicWhere
Limit which records of a model anyone on the internet may read, e.g. published posts only; with no condition, every record.
smithable op setPublicWhere <model> [where]
<model>(a model's name)<where>(key=value pairs, optional): Field values a record must have to be read by visitors, e.g. { "status": "published" }, or one of several, { "status": "investigating | identified" }; none means every record.
Never loses data.
Example: smithable op setPublicWhere Incident "status=identified | monitoring | resolved"
Look
addTheme
Add a theme of the project's own (themes/
smithable op addTheme <name> [--text …] [--use …]
<name>(text): The theme's name, kebab-case, like "forest".--text(text): The theme file's text: smithableTheme: 0.1, a font, at least three colors per mode (see the spec, Themes). On the command line, --text @path reads a file.--use(true or false, optional): Also make it the app's theme.
Never loses data.
setTheme
Choose the theme preset, brand color, background color, font, density or motion ("none" removes a color or the font; for motion it means no motion).
smithable op setTheme [--preset …] [--primary …] [--background …] [--font …] [--density …] [--motion …] [--page …]
--preset(text, optional): A built-in theme (modern, minimal, playful, corporate, coastal, navy, editorial, woodland, contrast, nordic, pastel, midnight, craft, restaurant, street-food, cafe, warm-service, quiet-workspace, ink-and-brass) or one of the project's themes/.yml. --primary(text, optional): A color like "#2563eb", or "none".--background(text, optional): A background color like "#f5efe6" (a wash in light and dark mode), or "none".--font(text, optional): Inter, Nunito Sans, Source Sans 3, Source Serif 4, Fraunces, Archivo Narrow, DM Sans, or "none".--density(comfortableorstandardorcompactornone, optional): How much space the lists and forms take; on a page, "none" returns to the app's.--motion(noneorsubtleorlively, optional): How much the app moves; a visitor's reduced-motion setting always wins.--page(a page's name, optional): A landing page given its own theme; without it, the app's theme.
Never loses data.
Example: smithable op setTheme --primary "#0f766e"
setSectionLook
Choose how a landing section looks, e.g. hero layout split|centered|plain, faq layout accordion|list ("default" removes it).
smithable op setSectionLook <page> <section> <key> <value>
<page>(a page's name)<section>(text): The section's name, e.g. hero.<key>(text): layout, image, columns or highlight, as the section takes.<value>(text): A value the key allows, or "default".
Never loses data.
setColumns
Choose which fields a page's table shows, in order (none: back to the default).
smithable op setColumns <page> [columns]
<page>(a page's name)<columns>(a field's name, optional)
Never loses data.
setFieldOrder
Order the fields of a page's form; fields not listed follow (none: the spec's order).
smithable op setFieldOrder <page> [fields]
<page>(a page's name)<fields>(a field's name, optional)
Never loses data.
setTone
Give a choice value a color by meaning, e.g. paid: success, overdue: warning (neutral: the default).
smithable op setTone <model> <field> <value> <tone>
<model>(a model's name)<field>(a field's name)<value>(a choice value)<tone>(neutralorsuccessorwarningordanger)
Never loses data.
setIcon
Choose the icon a page has in navigation ("auto": chosen from its model's name).
smithable op setIcon <page> <icon>
<page>(a page's name)<icon>(usersoruserorfileorreceiptorcartorpackageorcalendarorlayersorbookorbriefcaseorbuildingorchartorclipboardorhomeormailorsettingsorstarortagortruckorlistorauto)
Never loses data.
setBelow
Show a second field under a table column's value, e.g. phone under email ("none" removes it).
smithable op setBelow <page> <column> <field>
<page>(a page's name)<column>(a field's name)<field>(a field's name)
Never loses data.
setControl
How a field is entered on a page's form: slider, stepper, radio, select, combobox, textarea or switch ("input" is the default for its kind).
smithable op setControl <page> <field> <control>
<page>(a page's name)<field>(a field's name)<control>(inputorsliderorstepperorradioorselectorcomboboxortextareaorswitch): The control.
Never loses data.
setFormSections
Group a page's form into sections with headings, each "name:field,field"; fields in none follow at the end (none: one form).
smithable op setFormSections <page> [sections]
<page>(a page's name)<sections>(a list, optional): Each "name:field,field", e.g. "contact:name,email,phone".
Never loses data.
setFormColumns
Short fields of a page's form one under the other (1) or two abreast on wide screens (2).
smithable op setFormColumns <page> <columns>
<page>(a page's name)<columns>(1or2)
Never loses data.
setView
Show a page's list as a table or as cards on wide screens (a phone shows cards anyway).
smithable op setView <page> <view>
<page>(a page's name)<view>(tableorcards)
Never loses data.
setSort
The order of a page's list until someone sorts it: a field, or "-field" for the largest or latest first ("none": newest first).
smithable op setSort <page> <field>
<page>(a page's name)<field>(text): A field, or "-field" descending; "none" for the default.
Never loses data.
Texts
setText
Change a visible text, e.g. path [pages, customers, subtitle] or [models, Customer, fields, email, label] ("" restores the default).
smithable op setText <text> <path> [--language …]
<text>(text): The new text; "" restores the default.<path>(a list): Where in smithable.content.yml, e.g. [pages, customers, title].--language(enorsv, optional): In one of the app's other languages: its file, smithable.content..yml.
Never loses data.
Actions and queries
addAction
Add an action: standard with --on and --set (it sets fields of a record), else custom (its behavior is code in src/custom/actions/).
smithable op addAction <action> [input] [--on …] [--when …] [--set …] [--add …] [--copy …] [--with …] [--output …] [--intent …]
<action>(text): camelCase name, e.g. mergeCustomers.<input>(key=value pairs, optional): A custom action: the values it needs, as field shorthand, e.g. { keep: "Customer!" }.--on(a model's name, optional): A standard action: the model whose record it changes.--when(key=value pairs, optional): Only for records with these values, e.g. { status: "investigating | identified" } or { status: [investigating, identified] }.--set(key=value pairs, optional): The new values: a value, a formula, or nothing to ask the person, e.g. { status: canceled, cancelReason: "" }.--add(key=value pairs, optional): A related record to create, by model, e.g. { IncidentUpdate: { status: resolved, at: "NOW()", message: "" } }, or as Model.field pairs from the command line.--copy(key=value pairs, optional): A copy of the record instead of a change: what differs (a value, a formula, or nothing to ask), e.g. { status: draft, number: "" }; empty copies as is.--with(a list, optional): Lists of the model whose records the copy takes along, e.g. [lines].--output(key=value pairs, optional): A custom action: the values it returns, if any.--intent(text, optional): What it is for, in a sentence.
Never loses data.
Example: smithable op addAction mergeCustomers keep=Customer! duplicate=Customer!
setAction
Change an action: its intent, the records it is for (--when) or the values it sets (--set). Each replaces what was there; an empty one removes it. Who may run it is grant
smithable op setAction <action> [--intent …] [--when …] [--set …]
<action>(text): The action's name, e.g. mergeCustomers.--intent(text, optional): What it is for, in a sentence; "" removes it.--when(key=value pairs, optional): A standard action: only for records with these values, e.g. { status: "todo | doing" }; empty means every record.--set(key=value pairs, optional): A standard action: the new values, all of them, e.g. { status: done, doneOn: "TODAY()" }.
Never loses data.
removeAction
Remove an action, from the pages that offer it too. A custom action's code in src/custom/actions/ stays.
smithable op removeAction <action>
<action>(text): The action's name, e.g. mergeCustomers.
Never loses data.
addQuery
Add a custom query: records of a model chosen or ordered by code (e.g. customers most likely to churn). Its code is written afterwards, in src/custom/queries/.
smithable op addQuery <query> <returns> [--intent …]
<query>(text): camelCase name, e.g. churnRisk.<returns>(a model's name): The model whose records it returns, e.g. Customer.--intent(text, optional): What it finds and in which order, in one sentence.
Never loses data.
Data
importRecords
Add records from a CSV file (a header row; columns matched to fields by name or --map). --dry-run shows the first rows and every problem without adding anything.
smithable op importRecords <model> [--from …] [--text …] [--map …] [--dryRun …]
<model>(a model's name)--from(text, optional): The CSV file, e.g. customers.csv.--text(text, optional): The CSV itself, instead of a file.--map(key=value pairs, optional): Columns for fields where the names differ, e.g. { "Company": "name" }.--dryRun(true or false, optional): Show what would be added; add nothing.
Never loses data.
Example: smithable op importRecords Customer --from customers.csv
The app as a whole
setRuleId
Give a rule a stable id, so code and tests can say they implement it.
smithable op setRuleId <concept> <number> <id>
<concept>(text): e.g. "action:mergeCustomers", "model:Order" or "field:Order.total".<number>(text): The rule's position in the list, from 1.<id>(text): A short name, e.g. "move-orders".
Never loses data.
editSpec
Replace smithable.yml, or one of its companion files, with an edited version, as a whole.
smithable op editSpec [text] [--path …] [--value …] [--file …]
<text>(text, optional): The complete new file (without path).--path(a list, optional): Only this part of the file, e.g. [app, timezone] or [pages, menu, order, lead].--value(text, optional): The part's new value with path; null takes the part away.--file(smithable.ymlorsmithable.auth.ymlorsmithable.ux.ymlorsmithable.content.ymlorsmithable.context.yml, optional): Which file; smithable.yml when not said.
Can lose data in some cases (removing or narrowing what holds records); then it asks first, or takes --yes.
setPackage
Add a library for custom code at an exact version, e.g. chart.js 4.4.1 ("none" removes it).
smithable op setPackage <name> <version>
<name>(text): The npm package, e.g. chart.js.<version>(text): An exact version, e.g. 4.4.1, or "none".
Never loses data.
Example: smithable op setPackage chart.js 4.4.1
setApp
Set what the app is: its name, a one-sentence description, its currency (ISO 4217) or time zone ("none" removes the currency or time zone).
smithable op setApp [--name …] [--description …] [--currency …] [--timezone …]
--name(text, optional): The app's name, e.g. Stillwater Yoga.--description(text, optional): One sentence on what the app is for.--currency(text, optional): ISO 4217 code, e.g. EUR, or "none".--timezone(text, optional): IANA time zone, e.g. Europe/Mariehamn, or "none".
Never loses data.
Other changes
setOwner
Say whose a record is: the user in one of its User fields (the account manager, the salesperson who handles it) instead of whoever created it. Without a field, the creator again. "The owner" in the access rules is then that user.
smithable op setOwner <model> [field]
<model>(a model's name)<field>(a field's name, optional): A field of the model that refers to User, e.g. accountManager. Leave it out to make the creator the owner again.
Never loses data.
setFrame
Which frame a page shows in: site (the public frame with the top menu), app (the workspace with the side menu) or none (the page alone); default returns to its kind's.
smithable op setFrame <page> <frame>
<page>(a page's name)<frame>(siteorappornoneordefault): site: the public frame; app: the workspace; none: no menu and no header; default: the page kind's own.
Never loses data.
Example: smithable op setFrame about site
setRecordList
Show a list of related records on a page's record as a thread (a conversation with a reply box) or a table (the default).
smithable op setRecordList <page> <field> <look>
<page>(a page's name)<field>(a field's name)<look>(threadortable): thread or table.
Never loses data.
Example: smithable op setRecordList customers notes thread
setRecordLines
What a page shows under a record's name on cards and in pickers (docs/design/record-names.md §3): a subtitle and detail parts, each a field, a reference or a field one reference away; "none" takes the subtitle away, an empty detail list the details.
smithable op setRecordLines <page> [subtitle] [--detail …]
<page>(a page's name)<subtitle>(text, optional): The line under the name, e.g. instructor.name; "none" removes it.--detail(a list, optional): Smaller parts after it, e.g. classType.level capacity; none (or no parts) removes them.
Never loses data.
Example: smithable op setRecordLines sessions instructor.name --detail classType.level
setAiBadge
Mark pictures an AI made with "AI-generated" or "AI" (true, the default) or not (false): whether your audience needs it is your responsibility.
smithable op setAiBadge [shown]
<shown>(trueorfalse, optional): true (default) or false.
Never loses data.
Example: smithable op setAiBadge false
setAssistantVoice
Set the manner of Ask, the assistant in the app: plain (the default: short, no pleasantries), friendly or formal.
smithable op setAssistantVoice <voice>
<voice>(plainorfriendlyorformal): plain, friendly or formal.
Never loses data.
Example: smithable op setAssistantVoice friendly
setFiltersShown
Which of a page's filters show when the list is first reached; the rest fold behind "More filters" (no fields: every filter shows).
smithable op setFiltersShown <page> [fields]
<page>(a page's name)<fields>(a list, optional): Filters of the page (smithable.yml's filters), in the order shown.
Never loses data.
Example: smithable op setFiltersShown customers segment creditLimit
setTexts
Change several visible texts at once, each by its path in dots, e.g. { "pages.customers.title": "Kunder" }; with language, in that language file. A key with a dot in it (a choice value like "e.g.") cannot be reached this way: use setText.
smithable op setTexts [--texts …] [--language …]
--texts(key=value pairs): Path in dots → text, e.g. { "models.Customer.label": "Kund" }; "" restores the default.--language(enorsv, optional): In one of the app's other languages: its file, smithable.content..yml.
Never loses data.
nameRecords
Call a kind of record by other names, for one and for several, and retitle its list pages that carried the old name.
smithable op nameRecords <model> <one> [several]
<model>(a model's name)<one>(text): The name for one, e.g. "Job".<several>(text, optional): The name for several, e.g. "Jobs"; without it, the plural of the name for one.
Never loses data.
setLanguage
The language of everything a person reads in the app: en, sv.
smithable op setLanguage <language>
<language>(enorsv): A language the app kit has words in. The maker's own texts stay as written.
Never loses data.
Example: smithable op setLanguage sv
addLanguage
Offer the app in another language too, with its own file of the app's texts to fill in: en, sv.
smithable op addLanguage <language>
<language>(enorsv): A language the app kit has words in, other than the app's own.
Never loses data.
Example: smithable op addLanguage sv
removeLanguage
Stop offering the app in one of its other languages; its file of texts goes too.
smithable op removeLanguage <language>
<language>(enorsv): One of the app's other languages.
Can lose data in some cases (removing or narrowing what holds records); then it asks first, or takes --yes.
Example: smithable op removeLanguage sv
setLogo
The app's mark, a file under public/images/ shown before its name and as its tab icon ("" removes it).
smithable op setLogo [src] [--alt …]
<src>(text, optional): The file, e.g. images/logo.webp (webp, avif, jpg, png or svg); none removes it.--alt(text, optional): What the mark shows; without it, the one it has, else the app's name.
Never loses data.
Example: smithable op setLogo images/logo.webp --alt "A lotus"
setAssistantTexts
Set the texts of Ask, the assistant in the app: its name ("Ask" by default), the first line it shows, the question box's placeholder, and one sentence of what it is for (shown and given to the model); "" clears one.
smithable op setAssistantTexts [--name …] [--greeting …] [--placeholder …] [--about …]
--name(text, optional): The panel's name, e.g. Nova; "" for "Ask".--greeting(text, optional): The first line it shows.--placeholder(text, optional): The question box's placeholder.--about(text, optional): One sentence of what it is for.
Never loses data.
setDomains
Let only addresses on these email domains sign up (allowedDomains, e.g. acme.com); none (or no domains) lifts it. The admins may always sign in.
smithable op setDomains <domains>
<domains>(a list): The domains, e.g. [acme.com]; [] or ["none"] lets any address sign up.
Never loses data.
Example: smithable op setDomains acme.com
setAdmins
Say who the administrators are, by email address: they become admins when they first sign in, and may always sign in.
smithable op setAdmins <admins>
<admins>(a list): The administrators' email addresses, e.g. [you@your-domain].
Never loses data.
Example: smithable op setAdmins you@your-domain
setProviders
Choose how people sign in, in button order: email (a code by mail, needs only the app's mail settings), google or facebook (each needs its keys in the environment, see the README).
smithable op setProviders <providers>
<providers>(a list): The sign-in methods in button order, e.g. [email, google].
Never loses data.
Example: smithable op setProviders email google
setApi
Say what other programs may reach with a key: models ("all" for every model, also later ones), the standard actions they may run, and whether AI assistants may use it too. No models turns the API off. A key's role still decides what it may do.
smithable op setApi [models] [--actions …] [--except …] [--assistants …] [--assistantsDelete …]
<models>(a list, optional): The models offered, e.g. [Customer, Order], or [all]; none turns the API off.--actions(a list, optional): The standard actions programs may run, e.g. [resolve], or [all].--except(a list, optional): With "all": the models left out, e.g. [AuditEntry].--assistants(true or false, optional): Offer the same to AI assistants as tools.--assistantsDelete(true or false, optional): With assistants: they may also delete records, where the key's role may. Off unless said: an assistant can be talked into things by the text it reads.
Never loses data.
Example: smithable op setApi Customer Order --actions markPaid
setAssistant
Say who gets "Ask", the assistant in the app: some roles, signed-in (everyone who signs in) or anyone (visitors too, who are answered without a model); and whether it only reads or may also change things, each change confirmed by the person. Nobody turns it off. It never deletes.
smithable op setAssistant [for] [--may …]
<for>(a list, optional): Roles, e.g. [admin, staff]; or [signed-in]; or [anyone]. None turns it off.--may(readorchange, optional): read (the default): it finds and counts. change: it also adds, changes and runs actions.
Never loses data.
setAnalytics
Count visits with plausible, umami, fathom, google and the app's id there ("none" stops it); google asks visitors for consent first.
smithable op setAnalytics <provider> [id]
<provider>(plausibleorumamiorfathomorgoogleornone)<id>(text, optional): The app's id with the provider, e.g. shop.example.com for Plausible.
Never loses data.
Example: smithable op setAnalytics plausible shop.example.com
addTimer
Add a timer: an event the app raises at a time, on a clock (every day at 08:00, or cron) or relative to a date of a record (1 day before session.startsAt), running an action over the records it is due for; outgoing may listen to it.
smithable op addTimer <name> [--on …] [--at …] [--every …] [--cron …] [--where …] [--run …]
<name>(text): The timer's name, e.g. remind.--on(a model's name, optional): The model whose records it runs for; a clock timer running a custom action has none.--at(text, optional): With "on": a date field of the records, alone or after an offset ("1 day before session.startsAt"); with "every": a clock time ("08:00").--every(dayorweekdayorweekormonthormondayortuesdayorwednesdayorthursdayorfridayorsaturdayorsunday, optional): A clock timer: day, weekday, week, month or a weekday's name, with at.--cron(text, optional): A five-field cron expression ("0 6 25 * *"), for what every cannot say.--where(key=value pairs, optional): Only records with these values, e.g. { "status": "confirmed" }.--run(text, optional): The action it runs: a standard action of the records' model, or a custom one (once).
Never loses data.
setTimer
Change a timer: its records, time, condition or action. "none" takes a key away (where, run, on); a new time form replaces the old.
smithable op setTimer <name> [--on …] [--at …] [--every …] [--cron …] [--where …] [--run …]
<name>(text)--on(a model's name, optional): The model whose records it runs for; a clock timer running a custom action has none.--at(text, optional): With "on": a date field of the records, alone or after an offset ("1 day before session.startsAt"); with "every": a clock time ("08:00").--every(dayorweekdayorweekormonthormondayortuesdayorwednesdayorthursdayorfridayorsaturdayorsunday, optional): A clock timer: day, weekday, week, month or a weekday's name, with at.--cron(text, optional): A five-field cron expression ("0 6 25 * *"), for what every cannot say.--where(key=value pairs, optional): Only records with these values, e.g. { "status": "confirmed" }.--run(text, optional): The action it runs: a standard action of the records' model, or a custom one (once).
Never loses data.
removeTimer
Remove a timer. An outgoing entry that listens to it is removed first.
smithable op removeTimer <name>
<name>(text)
Can lose data in some cases (removing or narrowing what holds records); then it asks first, or takes --yes.
addOutgoing
Tell someone when something happens to a record: mail the record's owner, a role or an address on the record when a record is added, changed or deleted, or an action runs (e.g. a confirmation when a booking is made).
smithable op addOutgoing <outgoing> <when> [--where …] [--fields …] [--to …] [--recipient …]
<outgoing>(text): camelCase name, e.g. bookingMade.<when>(text): What happens: "created", " changed" or " deleted", or the name of a standard action, e.g. "Booking created" or "resolve". --where(key=value pairs, optional): Only for records with these values, e.g. { status: confirmed }; a change counts when the record comes to fit them.--fields(a list, optional): For "changed": only a change of one of these fields counts, e.g. [startsAt]. --to(text, optional): "mail" (the default), or the name of a target whose address is a setting.--recipient(text, optional): For mail: "owner", a role (admin), an email field of the record (email), a field that refers to User (assignee), one reference away (customer.email), or a list field and then the owner or an address of each of its records (bookings.owner).
Never loses data.
Example: smithable op addOutgoing bookingMade "Booking created" --recipient owner
removeOutgoing
Stop telling someone when something happens: remove an outgoing mail or call.
smithable op removeOutgoing <outgoing>
<outgoing>(text): The name, e.g. bookingMade.
Never loses data.
addIncoming
Add an address other services call (/incoming/
smithable op addIncoming <name> [--create …] [--set …] [--custom …] [--signed …] [--as …] [--mail …] [--fields …] [--reply …] [--replyFields …] [--replySet …] [--replyThen …]
<name>(text): camelCase, e.g. monitorDown.--create(a model's name, optional): The model a call adds a record of.--set(a list, optional): field=value pairs, e.g. title="{check.name} is down" status=investigating.--custom(true or false, optional): Hand the call to src/custom/incoming/.ts. --signed(true or false, optional): A sha256 signature of the body, not the secret itself.--as(a role, optional): The role the call acts as (admin when not said).--mail(true or false, optional): The call is a mail that becomes a record (with --create and --fields).--fields(a list, optional): For a mail: part=field pairs, e.g. from=email subject=title body=body attachments=files.--reply(a model's name, optional): For a mail: the model a reply to one of its records becomes, e.g. Message.--replyFields(a list, optional): For a reply: part=field pairs, e.g. from=from body=body attachments=files.--replySet(a list, optional): For a reply: field=value pairs on it, e.g. direction=in.--replyThen(a list, optional): For a reply: field=value pairs on the record it joins, e.g. status=open.
Never loses data.
setIncomingReply
Say what a reply to one of an incoming mail's records becomes (--create Message --fields from=from body=body, --set on it, --then on the record it joins), or --none to make every mail a new record again.
smithable op setIncomingReply <name> [--create …] [--fields …] [--set …] [--then …] [--none …]
<name>(text)--create(a model's name, optional): For a mail: the model a reply to one of its records becomes, e.g. Message.--fields(a list, optional): For a reply: part=field pairs, e.g. from=from body=body attachments=files.--set(a list, optional): For a reply: field=value pairs on it, e.g. direction=in.--then(a list, optional): For a reply: field=value pairs on the record it joins, e.g. status=open.--none(true or false, optional): No replies: every mail becomes a new record.
Never loses data.
removeIncoming
Remove an incoming call: its address answers no more. Custom code for it stays.
smithable op removeIncoming <name>
<name>(text)
Never loses data.