[{"data":1,"prerenderedAt":477},["ShallowReactive",2],{"content-query-F5rwjK5B9N":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":7,"summary":9,"order":10,"body":11,"_type":471,"_id":472,"_source":473,"_file":474,"_stem":475,"_extension":476},"\u002Fguides\u002Ffsm-as-the-spine","guides",false,"","FSM as the spine of operations software — FastYoke","Why every serious operations app eventually grows a state machine — and what changes when you start with one instead of bolting it on later.",7,{"type":12,"children":13,"toc":455},"root",[14,22,430],{"type":15,"tag":16,"props":17,"children":21},"element","hero",{"eyebrow":18,"tagline":19,"title":20},"Architecture","The status column starts simple and quietly becomes the load-bearing primitive of every operations app. Here's what changes when you treat it as one from the start.","FSM as the spine of operations software",[],{"type":15,"tag":23,"props":24,"children":27},"marketing-section",{"band":25,"max-width":26},"white","3xl",[28,36,42,71,76,89,101,107,114,135,140,146,159,164,170,191,196,202,215,220,226,238,243,249,261,289,294,300,305,346,351,357,405,411],{"type":15,"tag":29,"props":30,"children":32},"h2",{"id":31},"the-pattern-every-ops-app-converges-on",[33],{"type":34,"value":35},"text","The pattern every ops app converges on",{"type":15,"tag":37,"props":38,"children":39},"p",{},[40],{"type":34,"value":41},"Pick any operations-heavy application that's been in\nproduction for two years and you'll find some version of\nthis:",{"type":15,"tag":43,"props":44,"children":45},"ul",{},[46,52,66],{"type":15,"tag":47,"props":48,"children":49},"li",{},[50],{"type":34,"value":51},"A table that holds the records.",{"type":15,"tag":47,"props":53,"children":54},{},[55,57,64],{"type":34,"value":56},"A ",{"type":15,"tag":58,"props":59,"children":61},"code",{"className":60},[],[62],{"type":34,"value":63},"status",{"type":34,"value":65}," column on that table — a string, an enum, a\nforeign key into a \"stages\" lookup.",{"type":15,"tag":47,"props":67,"children":68},{},[69],{"type":34,"value":70},"A pile of code that reads and writes that column.",{"type":15,"tag":37,"props":72,"children":73},{},[74],{"type":34,"value":75},"That code starts small: when status moves from \"Pending\"\nto \"Assigned,\" send a notification. Then it grows: only\nallow \"Assigned → InProgress\" if the assignee is\nqualified. Then: don't allow \"Cancelled → Anything.\" Then:\nwhen \"Pending\" has been the status for more than two\ndays, escalate. Then: log every status change with who,\nwhen, and why.",{"type":15,"tag":37,"props":77,"children":78},{},[79,81,87],{"type":34,"value":80},"By the time the app is mature, that pile of code is ",{"type":15,"tag":82,"props":83,"children":84},"strong",{},[85],{"type":34,"value":86},"a\nstate machine pretending it isn't one",{"type":34,"value":88},". The transitions\nare scattered across handlers. The guards are\nre-implemented in three different places. The audit log\nis partial because two of the writes forgot to record\nthemselves.",{"type":15,"tag":37,"props":90,"children":91},{},[92,94,99],{"type":34,"value":93},"FastYoke makes the state machine the ",{"type":15,"tag":82,"props":95,"children":96},{},[97],{"type":34,"value":98},"primary primitive",{"type":34,"value":100},"\nof every workflow. Not a derived view of the data — the\ncontract the data has to obey.",{"type":15,"tag":29,"props":102,"children":104},{"id":103},"what-changes-when-you-start-with-an-fsm",[105],{"type":34,"value":106},"What changes when you start with an FSM",{"type":15,"tag":108,"props":109,"children":111},"h3",{"id":110},"_1-transitions-are-checked-not-hoped",[112],{"type":34,"value":113},"1. Transitions are checked, not hoped",{"type":15,"tag":37,"props":115,"children":116},{},[117,119,125,127,133],{"type":34,"value":118},"Every transition has explicit ",{"type":15,"tag":58,"props":120,"children":122},{"className":121},[],[123],{"type":34,"value":124},"from",{"type":34,"value":126}," and ",{"type":15,"tag":58,"props":128,"children":130},{"className":129},[],[131],{"type":34,"value":132},"to",{"type":34,"value":134}," states. The\nengine refuses any write that doesn't match a declared\ntransition. The \"Cancelled → Anything\" bug doesn't get\ncaught in code review — it gets caught at the schema\nlayer, because there's no transition declared and the\nwrite fails.",{"type":15,"tag":37,"props":136,"children":137},{},[138],{"type":34,"value":139},"This isn't bureaucracy. It's the same lock that the type\nsystem gives you for function signatures, applied to the\ndomain model.",{"type":15,"tag":108,"props":141,"children":143},{"id":142},"_2-guards-are-first-class",[144],{"type":34,"value":145},"2. Guards are first-class",{"type":15,"tag":37,"props":147,"children":148},{},[149,151,157],{"type":34,"value":150},"A guard is a JSONLogic expression evaluated against the\nentity's payload before the transition fires. \"Assigned →\nInProgress\" is gated by ",{"type":15,"tag":58,"props":152,"children":154},{"className":153},[],[155],{"type":34,"value":156},"{\"==\": [{\"var\": \"qualified\"}, true]}",{"type":34,"value":158},". The guard is part of the schema, not part of the\nhandler. Every code path that fires the transition gets\nthe guard for free.",{"type":15,"tag":37,"props":160,"children":161},{},[162],{"type":34,"value":163},"When you need richer guard logic, the scripting tier runs\nQuickJS inside a wasmtime sandbox. You can write a real\nfunction, version it, test it. It still lives in the\nschema, not in a handler.",{"type":15,"tag":108,"props":165,"children":167},{"id":166},"_3-the-audit-log-writes-itself",[168],{"type":34,"value":169},"3. The audit log writes itself",{"type":15,"tag":37,"props":171,"children":172},{},[173,175,181,183,189],{"type":34,"value":174},"Every successful transition appends a row to the\nevent log. No ",{"type":15,"tag":58,"props":176,"children":178},{"className":177},[],[179],{"type":34,"value":180},"UPDATE",{"type":34,"value":182},", no ",{"type":15,"tag":58,"props":184,"children":186},{"className":185},[],[187],{"type":34,"value":188},"DELETE",{"type":34,"value":190}," — append-only by\nconstruction. The auditor's question \"what happened to\nthis job between Tuesday and Friday?\" has a literal answer\nin the database, ordered, with payloads and actors.",{"type":15,"tag":37,"props":192,"children":193},{},[194],{"type":34,"value":195},"You don't have to remember to log it. The engine does.",{"type":15,"tag":108,"props":197,"children":199},{"id":198},"_4-side-effects-fire-on-transitions-not-on-writes",[200],{"type":34,"value":201},"4. Side effects fire on transitions, not on writes",{"type":15,"tag":37,"props":203,"children":204},{},[205,207,213],{"type":34,"value":206},"A webhook to a partner, a notification, a payment capture\n— these are ",{"type":15,"tag":208,"props":209,"children":210},"em",{},[211],{"type":34,"value":212},"consequences",{"type":34,"value":214}," of a transition, not\nconsequences of a row write. Declaring them at the\ntransition level means the webhook fires once when the\ntransition fires once, even if two operators race to flip\nthe status. The engine serializes the transition; the\nside effect inherits that guarantee.",{"type":15,"tag":37,"props":216,"children":217},{},[218],{"type":34,"value":219},"This kills the \"we fired the partner webhook twice\"\nclass of bug at the schema layer.",{"type":15,"tag":108,"props":221,"children":223},{"id":222},"_5-the-visual-builder-is-the-schema",[224],{"type":34,"value":225},"5. The visual builder is the schema",{"type":15,"tag":37,"props":227,"children":228},{},[229,231,236],{"type":34,"value":230},"FSM Designer ships in the platform. Operators can see\ntheir workflow as a graph, drag a transition, ship a new\nstate. The graph ",{"type":15,"tag":82,"props":232,"children":233},{},[234],{"type":34,"value":235},"is",{"type":34,"value":237}," the schema — there's no\n\"the visual designer drifted from the code\" failure mode\nbecause they share the same source of truth.",{"type":15,"tag":37,"props":239,"children":240},{},[241],{"type":34,"value":242},"For workflows simple enough to author this way, the\noperator never touches code. For workflows that need\ncode, the schema is still the authoritative document; the\ncode is the implementation of one guard or one side\neffect.",{"type":15,"tag":29,"props":244,"children":246},{"id":245},"what-this-lets-you-skip",[247],{"type":34,"value":248},"What this lets you skip",{"type":15,"tag":37,"props":250,"children":251},{},[252,254,259],{"type":34,"value":253},"The list of problems an FSM-first architecture ",{"type":15,"tag":82,"props":255,"children":256},{},[257],{"type":34,"value":258},"doesn't\nhave",{"type":34,"value":260}," is long:",{"type":15,"tag":43,"props":262,"children":263},{},[264,269,274,279,284],{"type":15,"tag":47,"props":265,"children":266},{},[267],{"type":34,"value":268},"No \"developer forgot the status check\" bugs.",{"type":15,"tag":47,"props":270,"children":271},{},[272],{"type":34,"value":273},"No \"the audit log is missing entries for X\" gaps.",{"type":15,"tag":47,"props":275,"children":276},{},[277],{"type":34,"value":278},"No \"the webhook fired three times\" race conditions.",{"type":15,"tag":47,"props":280,"children":281},{},[282],{"type":34,"value":283},"No \"the bulk-update tool moved 4,000 records into an\ninvalid state\" outages.",{"type":15,"tag":47,"props":285,"children":286},{},[287],{"type":34,"value":288},"No \"rebuild the audit log from server logs because the\napp didn't record it\" forensics.",{"type":15,"tag":37,"props":290,"children":291},{},[292],{"type":34,"value":293},"These aren't theoretical wins. Every operations team\nthat's been in production for three years has the scars\nfrom at least three of them.",{"type":15,"tag":29,"props":295,"children":297},{"id":296},"what-this-costs",[298],{"type":34,"value":299},"What this costs",{"type":15,"tag":37,"props":301,"children":302},{},[303],{"type":34,"value":304},"The honest tradeoffs:",{"type":15,"tag":43,"props":306,"children":307},{},[308,318,336],{"type":15,"tag":47,"props":309,"children":310},{},[311,316],{"type":15,"tag":82,"props":312,"children":313},{},[314],{"type":34,"value":315},"You think about the workflow upfront.",{"type":34,"value":317}," Schema-first\ndesign asks you to enumerate the states and transitions\nbefore you start writing handlers. That's\nupstream work the \"add a status column and figure it\nout later\" approach doesn't make you do.",{"type":15,"tag":47,"props":319,"children":320},{},[321,326,328,334],{"type":15,"tag":82,"props":322,"children":323},{},[324],{"type":34,"value":325},"One-off operations get heavier.",{"type":34,"value":327}," Bulk-updating\n10,000 records to a new state requires the bulk\noperation to declare itself as a transition. That's\ngood for safety; it's slower than a ",{"type":15,"tag":58,"props":329,"children":331},{"className":330},[],[332],{"type":34,"value":333},"UPDATE ... SET status = ?",{"type":34,"value":335}," would be.",{"type":15,"tag":47,"props":337,"children":338},{},[339,344],{"type":15,"tag":82,"props":340,"children":341},{},[342],{"type":34,"value":343},"The mental model is graph-shaped, not list-shaped.",{"type":34,"value":345},"\nEngineers used to thinking \"records and statuses\"\nhave to rewire to \"entities and transitions.\" The\npayoff is real, but there's a ramp.",{"type":15,"tag":37,"props":347,"children":348},{},[349],{"type":34,"value":350},"If those costs are deal-breakers for your team's\npreferred shape, FastYoke is the wrong substrate. If they\nread as \"yes, finally,\" you're the audience.",{"type":15,"tag":29,"props":352,"children":354},{"id":353},"where-this-shows-up-across-fastyoke",[355],{"type":34,"value":356},"Where this shows up across FastYoke",{"type":15,"tag":43,"props":358,"children":359},{},[360,370,387],{"type":15,"tag":47,"props":361,"children":362},{},[363,368],{"type":15,"tag":82,"props":364,"children":365},{},[366],{"type":34,"value":367},"Every marketplace app",{"type":34,"value":369}," is built around one or more\nFSMs. Patient flow, warehouse management, AR\ninvoicing, field service — same primitive, different\nworkflow shape.",{"type":15,"tag":47,"props":371,"children":372},{},[373,378,380,385],{"type":15,"tag":82,"props":374,"children":375},{},[376],{"type":34,"value":377},"The admin \"cancel override\"",{"type":34,"value":379}," sits outside the FSM\non purpose, because cancellation is a privileged\nbypass — not a workflow transition. The schema says\nwhat the workflow is; the override says what an\nadmin can do ",{"type":15,"tag":208,"props":381,"children":382},{},[383],{"type":34,"value":384},"despite",{"type":34,"value":386}," the workflow.",{"type":15,"tag":47,"props":388,"children":389},{},[390,395,397,403],{"type":15,"tag":82,"props":391,"children":392},{},[393],{"type":34,"value":394},"Self-loop transitions",{"type":34,"value":396}," (where ",{"type":15,"tag":58,"props":398,"children":400},{"className":399},[],[401],{"type":34,"value":402},"from == to",{"type":34,"value":404},") are a\nlegitimate primitive for audit-only events,\nidempotent retries, and counter increments. The\nschema accommodates the cases the textbook doesn't.",{"type":15,"tag":29,"props":406,"children":408},{"id":407},"the-honest-summary",[409],{"type":34,"value":410},"The honest summary",{"type":15,"tag":37,"props":412,"children":413},{},[414,416,421,423,428],{"type":34,"value":415},"FSMs aren't the right shape for every application. They\n",{"type":15,"tag":82,"props":417,"children":418},{},[419],{"type":34,"value":420},"are",{"type":34,"value":422}," the right shape for operations software, because\noperations work is ",{"type":15,"tag":208,"props":424,"children":425},{},[426],{"type":34,"value":427},"journeys through states with rules",{"type":34,"value":429},".\nBolting a state machine onto an app that started without\none is the most common refactor on the planet. Starting\nwith one — and getting the audit log, the guards, the\nside effects, and the visual designer for free — is the\nunlock FastYoke is built around.",{"type":15,"tag":23,"props":431,"children":433},{"band":432,"max-width":26},"gray",[434],{"type":15,"tag":37,"props":435,"children":436},{},[437,439,445,447,453],{"type":34,"value":438},"For the procurement-grade detail on how state changes\nbecome the contractual audit trail, see\n",{"type":15,"tag":440,"props":441,"children":443},"a",{"href":442},"\u002Fsecurity",[444],{"type":34,"value":442},{"type":34,"value":446},". For an architectural conversation\nwith someone who'd be the engineer behind your tenant,\nrequest ",{"type":15,"tag":440,"props":448,"children":450},{"href":449},"\u002Fgetting-started",[451],{"type":34,"value":452},"early access",{"type":34,"value":454},".",{"title":7,"searchDepth":456,"depth":456,"links":457},2,[458,459,467,468,469,470],{"id":31,"depth":456,"text":35},{"id":103,"depth":456,"text":106,"children":460},[461,463,464,465,466],{"id":110,"depth":462,"text":113},3,{"id":142,"depth":462,"text":145},{"id":166,"depth":462,"text":169},{"id":198,"depth":462,"text":201},{"id":222,"depth":462,"text":225},{"id":245,"depth":456,"text":248},{"id":296,"depth":456,"text":299},{"id":353,"depth":456,"text":356},{"id":407,"depth":456,"text":410},"markdown","content:guides:fsm-as-the-spine.md","content","guides\u002Ffsm-as-the-spine.md","guides\u002Ffsm-as-the-spine","md",1783575469971]