smithery.ai

gleam-erlang-interop

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.

First seen Apr 29, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Declared agents claude-code

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,424 B
  • docs SUMMARY.md 252 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Gleam Erlang Interop Skill

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.

Primary Sources

  1. Gleam Externals Documentation - Official FFI guide
  2. Gleam Erlang Package - Erlang integration utilities
  3. Gleam OTP - OTP framework integration
  4. Gleam FAQ - Elixir Integration - Using Elixir code

Erlang Target

Building for Erlang

gleam build  # Defaults to Erlang target
gleam build --target erlang

Output in build/dev/erlang/.

Running Erlang Builds

gleam run  # Run your main function
erl -pa build/dev/erlang/*/ebin  # Start Erlang shell with your code

@external Attribute Syntax

Syntax: @external(erlang, "module", "function")

  • module: Erlang module name (lowercase with underscores)
  • function: Erlang function name
  • Type annotations are MANDATORY

Basic Erlang External

@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

Type Safety and Testing (CRITICAL)

The Compiler Cannot Verify External Types

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.

Write More Tests (MANDATORY)

Because externals bypass compiler analysis:

  • Write more unit tests than usual when using external functions
  • Test all edge cases thoroughly
  • Verify type conversions are correct
  • Test error handling paths
pub fn external_function_test() {
  let result = my_external_function("test")
  let assert Ok(value) = result
  assert value == expected
}

Data Type Mapping

Gleam to Erlang

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

Custom Type Representation

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)

Common Erlang Modules

Lists Module

@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)

String/Binary Module

@external(erlang, "string", "trim")
pub fn trim(s: String) -> String

@external(erlang, "string", "uppercase")
pub fn uppercase(s: String) -> String

Crypto Module

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.

Timer Module

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 Integration

Calling Elixir Modules

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

Using Elixir Macros

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

OTP Integration

Using gleam_otp

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
}

See: Gleam OTP Documentation

Atoms

import gleam/erlang/atom.{type Atom}

pub fn create_atom(name: String) -> Atom {
  atom.create_from_string(name)
}

See: gleamerlang - Atom

ETS (Erlang Term Storage)

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))

Bit Strings / Binary Data

Pattern Matching on Binaries

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

Bit Array Options

  • Size: value:32 (32 bits)
  • Type: int, float, bytes, bits, utf8, utf16, utf32
  • Signedness: signed, unsigned
  • Endianness: big, little, native
  • Unit: Multiplier for size
<<value:size(32)-unsigned-big-integer>> = data

Calling Erlang Libraries

HTTP Clients

For HTTP requests, prefer Gleam packages:

HTTP Servers

For web servers, prefer:

  • Mist - Native Gleam HTTP server
  • Wisp - Web framework

Database

For databases:

Compiling with Elixir Code

Mix Projects

Gleam code can be part of Elixir Mix projects:

  1. Add mix_gleam to dependencies
  2. Place Gleam code in src/
  3. Run mix compile

See: mixgleam

Using Gleam from Elixir

# Call Gleam module from Elixir
:my_gleam_module.my_function("arg")

Gleam module mypackage/mymodule becomes Erlang module :mypackage@mymodule.

Error Handling Interop

Converting Erlang Error Tuples

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

Erlang exceptions become Gleam panics. Wrap risky Erlang calls with proper error handling.

Multi-Target Support

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])
  }
}

Testing Erlang Integration

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.

Performance Considerations

Tail Call Optimization

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
  }
}

Binary Handling

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)
}

Debugging

Erlang Observer

Monitor your application:

erl -pa build/dev/erlang/*/ebin

Then in Erlang shell:

observer:start().

IO Debugging

import gleam/io

pub fn debug(value: a) -> a {
  io.debug(value)
  value
}

Best Practices

DO: Wrap FFI in Safe APIs (MANDATORY)

// 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)
}

DO: Document Platform Requirements (MANDATORY)

/// Hashes data using SHA-256.
///
/// **Platform**: Erlang only
/// **Requires**: Erlang crypto module
///
pub fn sha256(data: String) -> BitArray {
  // ...
}

DO: Test Extensively (MANDATORY)

Write comprehensive tests for all external functions.

DON'T: Skip Error Handling (FORBIDDEN)

Always wrap external calls with proper error handling.

DON'T: Use FFI for Simple Operations (ANTI-PATTERN)

// 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
}

Common Patterns

Wrapping Erlang Libraries

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](../../rules/external-functions.md)