FSL — a tiny, readable finite-state language for stateful communication UIs. TypeScript & Elixir. Sibling of the Elixip DSL.
2
stars
22
commits
TypeScript
primary language
Aug 19, 2026
updated
The current state of affairs: early design. Specs are written, code is coming. Now is exactly the right time to state your opinion.
FSL is a tiny, readable language for describing finite state machines, embedded in TypeScript and consumable from plain JavaScript. It exists to build stateful communication frontends — web phones, web chat, video relay, bot UIs — where the interface must follow the life of a call: registering, ringing, connected, failed, and every state in between.
No framework lock-in. No heavyweight runtime. Just states, events and transitions you can actually read.
calling_out: {
on: {
"sip:progress": () => loop("ringing"),
"sip:accepted": () => goto("connected", "200 OK"),
"ui:hangup": (_, ctx) => { ctx.session?.terminate();
return goto("ready", "caller gave up"); },
},
after: { delay: 90_000, then: () => goto("call_failed", "no answer") },
},
If you can read that, you already speak FSL.
FSL is the TypeScript sibling of the DSL at the heart of Elixip, an Elixir SIP framework where call scenarios are written as explicit state machines — declared states, declared transitions, events collected per state, readability above all (the Elixip FSL). That style has proven itself describing real SIP call flows on the backend; FSL brings the same discipline to the browser, adapted to how UIs actually work.
Communication UIs are state machines whether you admit it or not. The hangup button that stays clickable after the call ended, the call button that fires twice — these bugs are implicit state machines leaking. FSL makes the machine explicit:
goto, with a human-readable description that
becomes your debug log."*" clause flushes
it). The INVITE that arrives a millisecond before your state change stops
being a heisenbug.toMermaid() and see for yourself.fsl/react is one hook; other frameworks are ~30 lines
each. Vanilla JS just subscribes.goto("connectd") won't compile. JS users get the same
library with editor completion for free.Read the full picture in spec/fsl-js-ts.md and the roadmap in typescript/docs/implementation-plan.md.
Vanilla JavaScript — subscribe and render:
const phone = WebPhone.start();
bindSipStack(phone); // stack callbacks -> phone.send(...)
callButton.onclick = () =>
phone.send({ type: "ui:call", number: input.value });
phone.subscribe(({ state }) => renderUi(state));
React — one hook, no other coupling:
import { useMachine } from "finite-state-language/react";
function Phone() {
const { state, send } = useMachine(WebPhone);
return (
<CallButton
disabled={state !== "ready"}
onClick={() => send({ type: "ui:call", number })}
/>
);
}
FSL is a language first, an implementation second — this repository is structured accordingly:
spec/ the language: semantics, event model, design decisions
typescript/ the TypeScript implementation (npm: finite-state-language)
elixir/ reserved: the Elixir implementation, to be extracted from
Elixip's DSL (same states, same event model, no SIP coupling)
The two implementations must stay semantically aligned; the spec is the arbiter. When they diverge on purpose (JS has no process mailbox; the BEAM needs no pending queue), the divergence is documented in the spec.
FSL also reads as French Sign Language (LSF — Langue des Signes Française). That collision is embraced, not accidental: this project grew out of building total conversation services — audio, video and real-time text in the same call — so that deaf and hard-of-hearing users are first-class callers, not an afterthought. Telecoms should be inclusive by design. If FSL helps one more team ship a video-relay or sign-language service, the pun has done its job.
finite-state-language/diagram)fx.sbb / fx.sbbReturn)finite-state-language@0.2.0 on npmThis is the ground floor — the API is still soft, which means your use case can still shape it. Open an issue, challenge the spec, tell us about the communication UI you wish were easier to build — the ground rules are in CONTRIBUTING.md. Come help us make FSL... fully operational. We'd love to have you — no state secrets here.
22 commits
TypeScript
98.6%
JavaScript
1.4%
FSL — a tiny, readable finite-state language for stateful communication UIs. TypeScript & Elixir. Sibling of the Elixip DSL.
2
stars
22
commits
TypeScript
primary language
Aug 19, 2026
updated
The current state of affairs: early design. Specs are written, code is coming. Now is exactly the right time to state your opinion.
FSL is a tiny, readable language for describing finite state machines, embedded in TypeScript and consumable from plain JavaScript. It exists to build stateful communication frontends — web phones, web chat, video relay, bot UIs — where the interface must follow the life of a call: registering, ringing, connected, failed, and every state in between.
No framework lock-in. No heavyweight runtime. Just states, events and transitions you can actually read.
calling_out: {
on: {
"sip:progress": () => loop("ringing"),
"sip:accepted": () => goto("connected", "200 OK"),
"ui:hangup": (_, ctx) => { ctx.session?.terminate();
return goto("ready", "caller gave up"); },
},
after: { delay: 90_000, then: () => goto("call_failed", "no answer") },
},
If you can read that, you already speak FSL.
FSL is the TypeScript sibling of the DSL at the heart of Elixip, an Elixir SIP framework where call scenarios are written as explicit state machines — declared states, declared transitions, events collected per state, readability above all (the Elixip FSL). That style has proven itself describing real SIP call flows on the backend; FSL brings the same discipline to the browser, adapted to how UIs actually work.
Communication UIs are state machines whether you admit it or not. The hangup button that stays clickable after the call ended, the call button that fires twice — these bugs are implicit state machines leaking. FSL makes the machine explicit:
goto, with a human-readable description that
becomes your debug log."*" clause flushes
it). The INVITE that arrives a millisecond before your state change stops
being a heisenbug.toMermaid() and see for yourself.fsl/react is one hook; other frameworks are ~30 lines
each. Vanilla JS just subscribes.goto("connectd") won't compile. JS users get the same
library with editor completion for free.Read the full picture in spec/fsl-js-ts.md and the roadmap in typescript/docs/implementation-plan.md.
Vanilla JavaScript — subscribe and render:
const phone = WebPhone.start();
bindSipStack(phone); // stack callbacks -> phone.send(...)
callButton.onclick = () =>
phone.send({ type: "ui:call", number: input.value });
phone.subscribe(({ state }) => renderUi(state));
React — one hook, no other coupling:
import { useMachine } from "finite-state-language/react";
function Phone() {
const { state, send } = useMachine(WebPhone);
return (
<CallButton
disabled={state !== "ready"}
onClick={() => send({ type: "ui:call", number })}
/>
);
}
FSL is a language first, an implementation second — this repository is structured accordingly:
spec/ the language: semantics, event model, design decisions
typescript/ the TypeScript implementation (npm: finite-state-language)
elixir/ reserved: the Elixir implementation, to be extracted from
Elixip's DSL (same states, same event model, no SIP coupling)
The two implementations must stay semantically aligned; the spec is the arbiter. When they diverge on purpose (JS has no process mailbox; the BEAM needs no pending queue), the divergence is documented in the spec.
FSL also reads as French Sign Language (LSF — Langue des Signes Française). That collision is embraced, not accidental: this project grew out of building total conversation services — audio, video and real-time text in the same call — so that deaf and hard-of-hearing users are first-class callers, not an afterthought. Telecoms should be inclusive by design. If FSL helps one more team ship a video-relay or sign-language service, the pun has done its job.
finite-state-language/diagram)fx.sbb / fx.sbbReturn)finite-state-language@0.2.0 on npmThis is the ground floor — the API is still soft, which means your use case can still shape it. Open an issue, challenge the spec, tell us about the communication UI you wish were easier to build — the ground rules are in CONTRIBUTING.md. Come help us make FSL... fully operational. We'd love to have you — no state secrets here.
22 commits
TypeScript
98.6%
JavaScript
1.4%