Rerouted (retagged, needs approval). The tag now points at a different commit than when this version was published. The Lamdera package server serves a copy recovered from https://github.com/421anon/elm-flow.
What we checked: Tag 2.1.0 is still on github.com/421anon/elm-flow; its commit a0f12ea9a25f is from 2026-03-20 12:05 UTC, after the version was published (2026-03-20 11:50 UTC); elm.json is byte-identical to package.elm-lang.org's; the exposed modules match docs.json; the zip's SHA-1 is not the published one and nothing else confirms its contents.
We could not verify this copy against what was published, so the Lamdera compiler asks before using it (y/n). Without a terminal, as in CI, it stops with an error unless approved:
lamdera make --auto-approve-package-rerouting approves rerouted versions for that run,LAMDERA_AUTO_APPROVE_PACKAGE_REROUTING=1 does the same for every run in that environment,LAMDERA_CACHE=off skips the Lamdera package server altogether: packages come from package.elm-lang.org and GitHub only, as with elm, and a rerouted version then fails to download.Details · package.elm-lang.org's zip: https://github.com/421anon/elm-flow/zipball/2.1.0/
A monadic interface for The Elm Architecture, bridging synchronous state modifications with asynchronous subscriptions via Channels.
This library merges concepts from two packages:
chrilves/elm-io — the free monad interface for TEAbrian-watkins/elm-procedure — continuation-passing channels and subscriptionsWe wish to express our deep respect and heartfelt thanks to the authors of these packages. elm-flow is largely an amalgamation of the two enabled by the generosity of their authors, publishing these under the permissive MIT and BSD-3 licenses, respectively. We hereby republish our entire derivative work under BSD-3.
Flow s a is a free monad that describes a computation which can:
sCmds and receive their resultsBatchThe runtime interpreter (Flow.Program) drives these computations inside a standard TEA update loop, so the rest of your Elm app stays unchanged.
With elm-flow, you do not define a custom Msg type or an update function.
Instead, your view handlers and subscriptions emit Flow values directly,
and the library runtime interprets them.
import Flow exposing (Flow)
import Html
import Html.Events
type alias Model =
{ count : Int }
main : Flow.Program () Model Never
main =
Flow.element
{ init = \_ -> ( { count = 0 }, Flow.none )
, view = view
, subscriptions = \_ -> Sub.none
}
view : Model -> Html.Html (Flow Model ())
view model =
Html.button
[ Html.Events.onClick (Flow.modify (\m -> { m | count = m.count + 1 })) ]
[ Html.text ("Count: " ++ String.fromInt model.count) ]Channels connect external event sources (ports, WebSockets, timers) to a Flow pipeline. Use Flow.Channel.connect to build a channel from an Elm subscription port and an optional command port, then pass it to Flow.await (receive one value) or Flow.subscribe (handle values indefinitely).
-- In Ports.elm
port onMessage : (String -> msg) -> Sub msg
port send : String -> Cmd msg
myChannel : Channel Model String
myChannel =
Flow.Channel.connect
(\callback -> Ports.onMessage (\s -> callback s))
(\_ -> Ports.send "subscribe")
listenLoop : Flow Model ()
listenLoop =
Flow.subscribe
(\msg -> Flow.modify (\m -> { m | lastMessage = msg }))
myChannelInstead of creating a new port for every JavaScript interaction, you can use Flow.ffi to call many JS functions by name using a single pair of ports.
You provide the JS function name, an encoder for the outgoing data, and a decoder for the return value.
-- 1. Define one pair of ports
port ffiOut : { key : String, fn : String, value : Json.Encode.Value } -> Cmd msg
port ffiIn : ({ key : String, value : Json.Decode.Value } -> msg) -> Sub msg
-- 2. Wire them into a helper
callJs : String -> (a -> Json.Encode.Value) -> Json.Decode.Decoder b -> a -> Flow s b
callJs =
Flow.ffi Ports.ffiOut Ports.ffiIn
-- 3. Call any JS function by name
fetchData : String -> Flow Model ()
fetchData id =
callJs "fetchData" Json.Encode.string decodeData id
|> Flow.andThen handleData// On the JavaScript side:
app.ports.ffiOut.subscribe(async (req) => {
if (req.fn === "fetchData") {
const data = await fetch("/api/data/" + req.value).then(r => r.json());
// Return the data using the exact same key
app.ports.ffiIn.send({ key: req.key, value: data });
}
});Read state, do preparatory work, fire a command, recover on failure — all in a
single linear pipeline with no Msg variants or pattern-match dispatch:
runJob : Int -> Flow Model ()
runJob id =
Flow.get
|> Flow.andThen
(\model ->
-- flush any pending edits before running
Flow.when model.hasPendingEdits flushEdits
|> Flow.seq (setStatus id Loading)
|> Flow.seq (callApi (Api.start id model.config))
)
|> Flow.andThen
(\result ->
case result of
Ok _ ->
Flow.pure ()
Err _ ->
setStatus id Failed
)subscribe wires a channel to a handler function. When a single event carries
multiple updates, batchM fans them out into concurrent branches:
listenForUpdates : Int -> Flow Model ()
listenForUpdates roomId =
Flow.subscribe handleEvent (Channels.connect roomId)
handleEvent : ServerMessage -> Flow Model ()
handleEvent msg =
case msg of
Snapshot items ->
-- apply every item update as a concurrent branch
Flow.batchM (List.map applyUpdate items)
Heartbeat ->
Flow.pure ()
Error text ->
showToast textLenses give you composable read/write paths into nested data. The state monad gives you sequenced reads and writes. Put them together and you get something that reads like imperative mutation — but the result is a pure value, a data structure that can be passed around, combined, and transformed before a single effect ever runs.
We recommend watching the recording of a talk by Edward Kmett titled "Lenses: A Functional Imperative"
The optics helpers (try, forAll, over, setAll, via) work with
erlandsona/elm-accessors
lenses to target sub-fields without manual getter/setter boilerplate.
type alias Model =
{ user : { name : String, email : String, loginCount : Int }
}
-- Lenses: user, name, email, loginCount (defined once, composed with <<)
-- <...>
--
recordLogin : String -> String -> Flow Model ()
recordLogin newName newEmail =
Flow.setAll (user << name) newName
|> Flow.seq (Flow.setAll (user << email) (String.toLower newEmail))
|> Flow.seq (Flow.over (user << loginCount) ((+) 1))via zooms into a sub-model so you can write a self-contained Flow against it
and reuse it anywhere that sub-model appears:
-- operates only on User, with no knowledge of Model
normaliseUser : Flow User ()
normaliseUser =
Flow.over name String.trim
|> Flow.seq (Flow.over email String.toLower)
-- embed into the full model through the `user` lens
Flow.via user normaliseUserCore —
pure (lift a value),
lift (lift a Cmd),
get / set / modify (read and write the model),
andThen / map (sequence and transform),
batch / batchM (run multiple branches),
none (terminate / no-op).
Async —
await (suspend until a Channel delivers one value),
subscribe (handle every value indefinitely),
yield (force a render cycle before continuing),
async (fire-and-forget a sub-computation).
Control flow —
when (conditional execution),
bracket_ (acquire/release resources),
setting (hold a Bool lens True for a computation),
locking (skip if a Bool lens is already True).
BSD-3-Clause