From gleam
Guides Claude through integrating Gleam with Erlang and Elixir code using FFI, external functions, and BEAM libraries. Use when building for Erlang target, calling Erlang libraries, or integrating with existing BEAM systems.
How this skill is triggered — by the user, by Claude, or both
Slash command
/gleam:gleam-erlang-interopThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill guides Claude Code through integrating Gleam with Erlang and Elixir code.
This skill guides Claude Code through integrating Gleam with Erlang and Elixir code.
IMPORTANT: Always prefer pure Gleam solutions. Use externals only when there is no suitable alternative.
gleam build # Defaults to Erlang target
gleam build --target erlang
Output in build/dev/erlang/.
gleam run # Run your main function
erl -pa build/dev/erlang/*/ebin # Start Erlang shell with your code
Syntax: @external(erlang, "module", "function")
@external(erlang, "lists", "reverse")
pub fn reverse_list(list: List(element)) -> List(element)
This calls the Erlang lists:reverse/1 function.
See: External Functions - Erlang
From official documentation:
The Gleam compiler will ensure that all uses of the function will be correct for the annotated types, but it cannot verify that the function implemented in the other language returns the specified types.
You are responsible for ensuring the external function matches the types you declare.
Because externals bypass compiler analysis:
pub fn external_function_test() {
let result = my_external_function("test")
let assert Ok(value) = result
assert value == expected
}
| Gleam Type | Erlang Type | Notes |
|---|---|---|
Int | Integer | |
Float | Float | |
String | Binary | <<"text"/utf8>> |
Bool | Atom | true/false |
Nil | Atom | nil |
List(a) | List | [a] |
#(a, b) | Tuple | {a, b} |
Result(a, e) | Tagged tuple | {ok, a} or {error, e} |
| Custom types | Tagged tuples | |
BitArray | Binary |
pub type Status {
Loading
Success
Error
}
Maps to Erlang atoms: loading, success, error
PascalCase variants convert to snake_case atoms in Erlang.
pub type User {
Guest
LoggedIn(id: Int, name: String)
}
Maps to Erlang:
guest (single atom for variant without fields){logged_in, 123, <<"Alice"/utf8>>} (tagged tuple for variant with fields)@external(erlang, "lists", "reverse")
pub fn reverse(list: List(a)) -> List(a)
@external(erlang, "lists", "sort")
pub fn sort(list: List(a)) -> List(a)
@external(erlang, "lists", "flatten")
pub fn flatten(list: List(List(a))) -> List(a)
@external(erlang, "string", "trim")
pub fn trim(s: String) -> String
@external(erlang, "string", "uppercase")
pub fn uppercase(s: String) -> String
import gleam/dynamic.{type Dynamic}
@external(erlang, "crypto", "hash")
fn hash_ffi(algorithm: Dynamic, data: BitArray) -> BitArray
pub fn sha256(data: String) -> BitArray {
data
|> bit_array.from_string
|> hash_ffi(dynamic.from("sha256"), _)
}
@external(erlang, "crypto", "strong_rand_bytes")
pub fn random_bytes(n: Int) -> BitArray
Important: Atoms should be passed as Dynamic when calling Erlang functions.
import gleam/erlang/process
pub fn sleep(milliseconds: Int) -> Nil {
process.sleep(milliseconds)
}
Use gleam/erlang/process.sleep instead of calling timer:sleep directly.
Elixir modules require Elixir. prefix:
@external(erlang, "Elixir.String", "upcase")
pub fn upcase(string: String) -> String
@external(erlang, "Elixir.Enum", "map")
pub fn enum_map(list: List(a), f: fn(a) -> b) -> List(b)
Important: The target is still erlang, not elixir.
See: External Functions - Elixir
Elixir macros cannot be called from Gleam. They must be wrapped in regular functions.
# In Elixir (lib/my_wrapper.ex)
defmodule MyWrapper do
def use_macro(arg) do
# Call macro here
SomeMacro.macro_function(arg)
end
end
// In Gleam
@external(erlang, "Elixir.MyWrapper", "use_macro")
pub fn use_macro(arg: String) -> Result(Value, Error)
See: Gleam FAQ - Elixir Macros
For OTP functionality, use the gleam_otp package:
import gleam/otp/actor
import gleam/erlang/process.{type Subject}
pub fn start_worker() -> Result(Subject(Message), actor.StartError) {
actor.new(initial_state)
|> actor.on_message(handle_message)
|> actor.start
}
import gleam/erlang/atom.{type Atom}
pub fn create_atom(name: String) -> Atom {
atom.create_from_string(name)
}
See: gleam_erlang - Atom
import gleam/erlang/atom.{type Atom}
pub type Table
@external(erlang, "ets", "new")
pub fn new(name: Atom, options: List(Atom)) -> Table
@external(erlang, "ets", "insert")
pub fn insert(table: Table, objects: List(#(a, b))) -> Bool
@external(erlang, "ets", "lookup")
pub fn lookup(table: Table, key: a) -> List(#(a, b))
pub fn parse_packet(data: BitArray) -> Result(Packet, ParseError) {
case data {
<<version:8, type_:8, length:16, body:bytes>> -> {
Ok(Packet(version, type_, length, body))
}
_ -> Error(InvalidPacket)
}
}
See: Bit Array Syntax
value:32 (32 bits)int, float, bytes, bits, utf8, utf16, utf32signed, unsignedbig, little, native<<value:size(32)-unsigned-big-integer>> = data
For HTTP requests, prefer Gleam packages:
For web servers, prefer:
For databases:
Gleam code can be part of Elixir Mix projects:
mix_gleam to dependenciessrc/mix compileSee: mix_gleam
# Call Gleam module from Elixir
:my_gleam_module.my_function("arg")
Gleam module my_package/my_module becomes Erlang module :my_package@my_module.
import gleam/erlang/atom
@external(erlang, "file", "read_file")
fn do_read_file(path: String) -> Result(BitArray, Atom)
pub fn read_file(path: String) -> Result(String, FileError) {
case do_read_file(path) {
Ok(content) -> {
case bit_array.to_string(content) {
Ok(text) -> Ok(text)
Error(_) -> Error(InvalidEncoding)
}
}
Error(reason) -> Error(FileSystemError(atom.to_string(reason)))
}
}
Erlang exceptions become Gleam panics. Wrap risky Erlang calls with proper error handling.
Write code that works on both targets:
@external(erlang, "crypto", "strong_rand_bytes")
@external(javascript, "node:crypto", "randomBytes")
pub fn random_bytes(size: Int) -> BitArray
Or with fallback:
@external(erlang, "lists", "reverse")
pub fn reverse(list: List(a)) -> List(a) {
// Pure Gleam fallback
do_reverse(list, [])
}
fn do_reverse(remaining: List(a), reversed: List(a)) -> List(a) {
case remaining {
[] -> reversed
[x, ..xs] -> do_reverse(xs, [x, ..reversed])
}
}
pub fn erlang_interop_test() {
let reversed = reverse_list([1, 2, 3])
let assert [3, 2, 1] = reversed
let upper = uppercase("hello")
let assert "HELLO" = upper
}
Test both Gleam and Erlang implementations when available.
Erlang optimizes tail calls. Write recursive functions in tail position:
// Tail recursive (optimized)
pub fn sum(list: List(Int), acc: Int) -> Int {
case list {
[] -> acc
[x, ..rest] -> sum(rest, acc + x)
}
}
// Not tail recursive (uses stack)
pub fn sum_bad(list: List(Int)) -> Int {
case list {
[] -> 0
[x, ..rest] -> x + sum_bad(rest) // Not tail position
}
}
Binaries are efficient on BEAM. Use them for large data:
// Efficient: builds binary once
pub fn build_large_string(parts: List(String)) -> String {
string.concat(parts)
}
Monitor your application:
erl -pa build/dev/erlang/*/ebin
Then in Erlang shell:
observer:start().
import gleam/io
pub fn debug(value: a) -> a {
io.debug(value)
value
}
// Private FFI function
@external(erlang, "my_erlang_lib", "risky_function")
fn do_risky_function(arg: String) -> Dynamic
// Public safe wrapper
pub fn safe_function(arg: String) -> Result(Value, Error) {
do_risky_function(arg)
|> decode_response
|> result.map_error(error_to_string)
}
/// Hashes data using SHA-256.
///
/// **Platform**: Erlang only
/// **Requires**: Erlang crypto module
///
pub fn sha256(data: String) -> BitArray {
// ...
}
Write comprehensive tests for all external functions.
Always wrap external calls with proper error handling.
// Bad - unnecessary FFI
@external(erlang, "math", "add")
pub fn add(a: Int, b: Int) -> Int
// Good - pure Gleam
pub fn add(a: Int, b: Int) -> Int {
a + b
}
Create type-safe wrappers for Erlang code:
import gleam/dynamic.{type Dynamic}
// Low-level external
@external(erlang, "my_erlang_lib", "risky_function")
fn do_risky_function(arg: String) -> Dynamic
// Safe wrapper
pub fn safe_function(arg: String) -> Result(Value, Error) {
let decoder = dynamic.tuple2(
dynamic.string,
decode_value,
)
do_risky_function(arg)
|> decoder
|> result.then(fn(tuple) {
case tuple {
#("ok", value) -> Ok(value)
#("error", _) -> Error(OperationFailed)
_ -> Error(UnexpectedResponse)
}
})
|> result.map_error(fn(_) { DecodeFailed })
}
Remember: Gleam's Erlang target gives you full access to the BEAM ecosystem. Wrap external code for type safety.
See: External Functions
npx claudepluginhub renatillas/gleam --plugin gleamGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.