ports
Guide for Elixir/Erlang Ports — communicating with external OS processes. Use when spawning external programs, implementing port protocols, wrapping ports in GenServer, or choosing between Port, NIF, System.cmd, and C Nodes.
What this skill does
# Elixir Ports
Ports are Elixir's mechanism for communicating with external OS programs via stdin/stdout. A Port is not a network port — it is a process-like entity that manages an external OS program, forwarding messages as bytes and delivering responses back to the owning Elixir process.
## When to Use This Skill
Activate when:
- Spawning and communicating with external programs or scripts
- Implementing binary protocols over stdin/stdout
- Wrapping a port in a GenServer for lifecycle management
- Deciding between Port, NIF, C Node, or `System.cmd`
- Handling port crashes, exit status, or monitoring ports
- Working with long-running external processes
## Port Fundamentals
Open a port with `Port.open/2`. The most common name forms are `{:spawn, command}` and `{:spawn_executable, path}`.
```elixir
# Spawn by shell command string (uses shell, PATH lookups apply)
port = Port.open({:spawn, "cat"}, [:binary])
# Spawn executable directly (no shell, preferred for security)
port = Port.open({:spawn_executable, "/usr/bin/python3"}, [
:binary,
args: ["-u", "my_script.py"]
])
```
Send data to the port and receive from it:
```elixir
# Send bytes to the external process's stdin
Port.command(port, "hello\n")
# Receive response — messages arrive in the process mailbox
receive do
{^port, {:data, data}} ->
IO.puts("Got: #{data}")
end
```
Close the port explicitly or let it close when the owner process exits:
```elixir
Port.close(port)
# The port will reply {port, :closed} after the external process exits
```
## Port Options Reference
| Option | Description |
|--------|-------------|
| `:binary` | Deliver data as binaries (recommended; default is charlists) |
| `{:packet, N}` | Prefix each message with N-byte length header (N = 1, 2, or 4) |
| `{:line, L}` | Deliver complete lines up to L bytes; partial lines flagged |
| `:stream` | No framing — raw bytes delivered as received (default) |
| `:exit_status` | Send `{port, {:exit_status, code}}` when the program exits |
| `:use_stdio` | Use stdin/stdout for communication (default) |
| `:stderr_to_stdout` | Merge stderr into stdout stream |
| `{:cd, dir}` | Set working directory for the external process |
| `{:env, env_list}` | Set environment variables (REPLACES the entire environment) |
| `{:args, arg_list}` | Argument list when using `:spawn_executable` |
| `{:arg0, string}` | Override argv[0] for the external process |
| `:parallelism` | Hint to scheduler for improved throughput |
**Note on `{:env, env_list}`**: This option completely replaces the OS environment, it does not merge with the current environment. To extend the current environment, merge explicitly before passing it.
## Communication Patterns
### Stream Mode (default)
Bytes are delivered as they arrive with no framing. Use `:binary` to get binaries instead of charlists.
```elixir
port = Port.open({:spawn_executable, "/bin/cat"}, [:binary, :stream])
Port.command(port, "some data")
receive do
{^port, {:data, chunk}} -> IO.inspect(chunk)
end
```
### Packet Mode
Use `{:packet, N}` for message framing. The external program must also implement the same N-byte big-endian length prefix.
```elixir
# Elixir side
port = Port.open({:spawn_executable, "/usr/local/bin/my_worker"}, [
:binary,
{:packet, 4}
])
Port.command(port, encode_message("hello"))
receive do
{^port, {:data, response}} -> decode_message(response)
end
```
The external program reads a 4-byte big-endian integer as the message length, then reads that many bytes. It writes responses with the same framing.
### Line Mode
```elixir
port = Port.open({:spawn_executable, "/usr/bin/python3"}, [
:binary,
{:line, 4096},
args: ["script.py"]
])
receive do
{^port, {:data, {:eol, line}}} ->
IO.puts("Complete line: #{line}")
{^port, {:data, {:noeol, partial}}} ->
IO.puts("Partial line (too long): #{partial}")
end
```
### Receiving Exit Status
```elixir
port = Port.open({:spawn, "false"}, [:binary, :exit_status])
receive do
{^port, {:exit_status, code}} ->
IO.puts("Exited with code #{code}")
end
```
## GenServer Port Wrapper
Wrapping a port in a GenServer is the standard OTP pattern for managing external processes. The GenServer owns the port, handles messages, and exposes a clean API.
```elixir
defmodule MyApp.ExternalWorker do
use GenServer
@timeout 5_000
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def call(payload) do
GenServer.call(__MODULE__, {:call, payload}, @timeout)
end
# Server Callbacks
@impl true
def init(_opts) do
executable = System.find_executable("my_worker") ||
raise "my_worker not found in PATH"
port =
Port.open({:spawn_executable, executable}, [
:binary,
{:packet, 4},
:exit_status
])
ref = Port.monitor(port)
{:ok, %{port: port, ref: ref, caller: nil}}
end
@impl true
def handle_call({:call, payload}, from, %{port: port} = state) do
encoded = :erlang.term_to_binary(payload)
Port.command(port, encoded)
{:noreply, %{state | caller: from}}
end
@impl true
def handle_info({port, {:data, data}}, %{port: port, caller: caller} = state) do
response = :erlang.binary_to_term(data)
GenServer.reply(caller, {:ok, response})
{:noreply, %{state | caller: nil}}
end
@impl true
def handle_info({port, {:exit_status, code}}, %{port: port} = state) do
{:stop, {:port_exited, code}, state}
end
@impl true
def handle_info({:DOWN, ref, :port, _port, reason}, %{ref: ref} = state) do
{:stop, {:port_down, reason}, state}
end
@impl true
def terminate(_reason, %{port: port}) do
if Port.info(port) != nil do
Port.close(port)
end
:ok
end
end
```
Key points in this pattern:
- Store the port in GenServer state
- Monitor the port with `Port.monitor/1` to receive `:DOWN` messages
- Use `{:packet, N}` for reliable message framing
- Store the calling `from` pid when waiting for a response
- Reply via `GenServer.reply/2` from `handle_info` when data arrives
- Always check `Port.info(port)` before closing in `terminate/2`
### Supervision
Add the GenServer to a supervision tree:
```elixir
children = [
{MyApp.ExternalWorker, []},
# ...
]
Supervisor.start_link(children, strategy: :one_for_one)
```
When the external process crashes, the port sends an exit message, the GenServer stops, and the supervisor restarts it.
## Port Lifecycle and Error Handling
### Port Monitor
Use `Port.monitor/1` (available since Elixir v1.6) to receive a `{:DOWN, ref, :port, object, reason}` message when a port closes:
```elixir
port = Port.open({:spawn, "my_cmd"}, [:binary])
ref = Port.monitor(port)
receive do
{:DOWN, ^ref, :port, _port, reason} ->
IO.puts("Port closed: #{inspect(reason)}")
end
```
### Checking if a Port is Alive
```elixir
case Port.info(port) do
nil -> :port_closed
info -> {:alive, info}
end
```
### Port Ownership Transfer
Transfer ownership with `Port.connect/2`. The old owner receives `{port, :connected}`:
```elixir
Port.connect(port, new_owner_pid)
```
### Crash Semantics
- When the port owner process terminates, the port closes automatically, and the external program receives SIGTERM.
- When the external program exits, the port sends `{port, {:exit_status, code}}` (if `:exit_status` was set) and closes.
- Ports are linked to their owning process by default.
## Decision Guide: Port vs NIF vs C Node vs System.cmd
| Aspect | Port | NIF | C Node | System.cmd |
|--------|------|-----|--------|-----------|
| Crash isolation | Yes — OS process | No — crashes the VM | Yes — OS process | Yes — subprocess |
| Latency | IPC overhead | Near zero | Message passing | Blocking, high |
| Complexity | Low–Medium | High | Very high | Very low |
| Scheduler impact | None | Blocks (use dirty NIFs for >1ms) | None | Blocks caller only |
| Security boundary | OS process isolation | Full VM access | OS process isolatioRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.