SagaSaga
Guide

Interop

Saga compiles to Core Erlang and runs on the BEAM, so it can call any Erlang or Elixir library directly. This page covers the FFI: the @external annotation, bridge files for type-mismatched conventions, and the limitations on effectful callbacks. For declaring and installing dependencies (Hex, git, path), see Ecosystem.

Calling Erlang Functions

The @external annotation declares a function whose implementation lives in an Erlang module:

@external("erlang", "lists", "reverse")
pub fun reverse : List a -> List a

The three arguments are the target (always "erlang"), the Erlang module name, and the Erlang function name. A type signature is required. The compiler trusts it and emits a direct foreign call with no runtime validation.

When it works directly

If the Erlang function's argument and return types already match Saga's BEAM representations, no extra work is needed:

@external("erlang", "erlang", "length")
pub fun length : List a -> Int

@external("erlang", "maps", "put")
pub fun put : k -> v -> Dict k v -> Dict k v where {k: Eq}

This works for functions that take and return plain values (integers, floats, binaries, lists, maps), {ok, V} | {error, E} (matches Result), and true | false (matches Bool).

Bridge files

When an Erlang function's return convention doesn't match Saga's type representations, you write a bridge file: a .erl file that adapts between conventions.

For example, Erlang returns Value | undefined for optional values, but Saga represents Maybe as {just, V} | {nothing}. A bridge converts between these:

# Int.saga
@external("erlang", "my_int_bridge", "parse")
pub fun parse : String -> Maybe Int
%% my_int_bridge.erl
-module(my_int_bridge).
-export([parse/1]).

parse(S) ->
    case string:to_integer(S) of
        {N, []} -> {just, N};
        _ -> {nothing}
    end.

Place .erl bridge files anywhere in your project root (excluding _build/ and tests/). They are compiled alongside the generated Core Erlang files automatically.

The -module(name) in the .erl file must match the module string in @external.

Type representations

Bridge functions must return values matching these BEAM representations:

Saga TypeBEAM RepresentationExample
IntInteger42
FloatFloat1.5
StringBinary<<"hello">>
BoolAtoms true/falsetrue
UnitAtom unitunit
List aErlang list[1, 2, 3]
(a, b)Tuple{1, <<"hi">>}
Ok v{ok, V}{ok, <<"data">>}
Err e{error, E}{error, <<"fail">>}
Just v{just, V}{just, 42}
Nothing{nothing}{nothing}
Custom Foo x{module_Foo, X}{shapes_Circle, 5}

Note that Err maps to the atom error (not err), and Unit is the atom unit (not an empty tuple). Custom ADT constructors are prefixed with the module name in lowercase.

Limitation: effectful callbacks

Pure Saga functions can be passed across the FFI boundary and called from Erlang. But effectful functions (those with a needs clause) cannot. The compiler rewrites effectful functions into CPS form with extra hidden parameters, so an Erlang function that tries to call one will get an arity mismatch.

If you need a "wrap a callback in setup/teardown" pattern (transactions, locks, resource handles), expose separate acquire and release primitives from the bridge and call the Saga callback from Saga code, where the effect machinery is available. See the finally pattern in Handler Patterns for how this works in practice.