Jido.Signal (Jido Signal v2.0.0-rc.1)

View Source

Defines the core Signal structure in Jido, implementing the CloudEvents specification (v1.0.2) with Jido-specific extensions for agent-based systems.

https://cloudevents.io/

Overview

Signals are the universal message format in Jido, serving as the nervous system of your agent-based application. Every event, command, and state change flows through the system as a Signal, providing:

  • Standardized event structure (CloudEvents v1.0.2 compatible)
  • Rich metadata and context tracking
  • Flexible dispatch configuration
  • Automatic serialization

CloudEvents Compliance

Each Signal implements the CloudEvents v1.0.2 specification with these required fields:

  • specversion: Always "1.0.2"
  • id: Unique identifier (UUID v4)
  • source: Origin of the event ("/service/component")
  • type: Classification of the event ("domain.entity.action")

And optional fields:

  • subject: Specific subject of the event
  • time: Timestamp in ISO 8601 format
  • datacontenttype: Media type of the data (defaults to "application/json")
  • dataschema: Schema defining the data structure
  • data: The actual event payload

Jido Extensions

Beyond the CloudEvents spec, Signals support a flexible extension system for adding custom metadata and behavior. See Jido.Signal.Ext for details.

Creating Signals

Signals can be created in several ways (prefer the positional new/3):

alias Jido.Signal

# Preferred: positional constructor (type, data, attrs)
{:ok, signal} = Signal.new("metrics.collected", %{cpu: 80, memory: 70},
  source: "/monitoring"
)

# Also available: map/keyword constructor (backwards compatible)
{:ok, signal} = Signal.new(%{
  type: "user.created",
  source: "/auth/registration",
  data: %{user_id: "123", email: "user@example.com"}
})

# Data payloads follow CloudEvents rules:
# - When datacontenttype is JSON (or omitted in JSON format), `data` can be any JSON value
#   (object/map, array, string, number, boolean, null)
# - For non-JSON payloads, encode according to datacontenttype; binary payloads use data_base64 during JSON serialization

Custom Signal Types

You can define custom Signal types using the use Jido.Signal pattern:

defmodule MySignal do
  use Jido.Signal,
    type: "my.custom.signal",
    default_source: "/my/service",
    datacontenttype: "application/json",
    schema: [
      user_id: [type: :string, required: true],
      message: [type: :string, required: true]
    ]
end

# Create instances
{:ok, signal} = MySignal.new(%{user_id: "123", message: "Hello"})

# Override runtime fields
{:ok, signal} = MySignal.new(
  %{user_id: "123", message: "Hello"},
  source: "/different/source",
  subject: "user-notification"
)

Signal Types

Signal types are strings, but typically use a hierarchical dot notation:

<domain>.<entity>.<action>[.<qualifier>]

Examples:

  • user.profile.updated
  • order.payment.processed.success
  • system.metrics.collected

Guidelines for type naming:

  • Use lowercase with dots
  • Keep segments meaningful
  • Order from general to specific
  • Include qualifiers when needed

Data Content Types

The datacontenttype field indicates the format of the data field:

  • application/json (default) - JSON-structured data
  • text/plain - Unstructured text
  • application/octet-stream - Binary data
  • Custom MIME types for specific formats

Dispatch Configuration

Signal dispatch is configured when subscribing to the Bus or when calling Dispatch directly:

# Configure dispatch when subscribing
Bus.subscribe(bus, "user.*", dispatch: {:pubsub, topic: "events"})

# Or dispatch directly with config
Dispatch.dispatch(signal, {:logger, level: :info})

See Also

Summary

Functions

Defines a new Signal module.

Removes extension data from a Signal.

Deserializes binary data back into a Signal struct or list of Signal structs.

Flattens extension data to CloudEvents-compliant top-level attributes.

Creates a new Signal struct from a map.

Retrieves extension data from a Signal.

Inflates top-level attributes back to extension data.

Lists all extensions currently present on a Signal.

Converts a struct or list of structs to Signal data format.

Creates a new Signal struct.

Creates a Signal with explicit type and payload, plus optional attrs.

Creates a new Signal struct, raising an error if invalid.

Creates a new Signal struct with explicit type and data, raising an error if invalid.

Adds extension data to a Signal.

Returns the Zoi schema for Signal

Serializes a Signal or a list of Signals using the specified or default serializer.

Legacy serialize function that returns binary directly (for backward compatibility).

Types

t()

@type t() :: %Jido.Signal{
  data: any(),
  datacontenttype: binary(),
  dataschema: nil | binary(),
  extensions: map(),
  id: binary(),
  jido_dispatch: any(),
  source: binary(),
  specversion: binary(),
  subject: nil | binary(),
  time: nil | binary(),
  type: binary()
}

Functions

__using__(opts)

(macro)

Defines a new Signal module.

This macro sets up the necessary structure and callbacks for a custom Signal, including configuration validation and default implementations.

Options

  • :type (String.t/0) - Required. The type of the Signal

  • :default_source (String.t/0) - The default source of the Signal

  • :datacontenttype (String.t/0) - The content type of the data field

  • :dataschema (String.t/0) - Schema URI for the data field (optional)

  • :schema (keyword/0) - A NimbleOptions schema for validating the Signal's data parameters The default value is [].

Examples

defmodule MySignal do
  use Jido.Signal,
    type: "my.custom.signal",
    default_source: "/my/service",
    schema: [
      user_id: [type: :string, required: true],
      message: [type: :string, required: true]
    ]
end

delete_extension(signal, namespace)

@spec delete_extension(t(), String.t()) :: t()

Removes extension data from a Signal.

Removes the extension data stored under the given namespace and returns the updated Signal.

Parameters

  • signal - The Signal struct to remove from
  • namespace - The extension namespace (string)

Returns

The updated Signal struct with the extension removed

Examples

# Remove authentication extension
updated_signal = Jido.Signal.delete_extension(signal, "auth")

# Extension data is no longer present
nil = Jido.Signal.get_extension(updated_signal, "auth")

deserialize(binary, opts \\ [])

@spec deserialize(
  binary(),
  keyword()
) :: {:ok, t() | [t()]} | {:error, term()}

Deserializes binary data back into a Signal struct or list of Signal structs.

Parameters

  • binary: The serialized binary data to deserialize
  • opts: Optional configuration including:
    • :serializer - The serializer module to use (defaults to configured serializer)
    • :type - Specific type to deserialize to
    • :type_provider - Custom type provider

Returns

{:ok, Signal.t() | list(Signal.t())} if successful, {:error, reason} otherwise

Examples

# JSON deserialization (default)
iex> json = ~s({"type":"example.event","source":"/example","id":"123"})
iex> {:ok, signal} = Jido.Signal.deserialize(json)
iex> signal.type
"example.event"

# Using a specific serializer
iex> {:ok, signal} = Jido.Signal.deserialize(binary, serializer: Jido.Signal.Serialization.ErlangTermSerializer)
iex> signal.type
"example.event"

# Deserializing multiple Signals
iex> json = ~s([{"type":"event1","source":"/ex1"},{"type":"event2","source":"/ex2"}])
iex> {:ok, signals} = Jido.Signal.deserialize(json)
iex> length(signals)
2

flatten_extensions(signal)

@spec flatten_extensions(t()) :: map()

Flattens extension data to CloudEvents-compliant top-level attributes.

Takes a Signal struct and converts all extension data to top-level attributes by calling each extension's to_attrs/1 function and merging the results into the base signal map.

Parameters

  • signal - The Signal struct containing extensions to flatten

Returns

A map with extensions flattened to top-level CloudEvents attributes

Examples

signal = %Signal{type: "test", source: "/test", extensions: %{"auth" => %{user_id: "123"}}}
flattened = Jido.Signal.flatten_extensions(signal)
# => %{"type" => "test", "source" => "/test", "user_id" => "123", ...}

from_map(map)

@spec from_map(map()) :: {:ok, t()} | {:error, String.t()}

Creates a new Signal struct from a map.

Parameters

  • map: A map containing the Signal attributes.

Returns

{:ok, Signal.t()} if the map is valid, {:error, String.t()} otherwise.

Examples

iex> Jido.Signal.from_map(%{"type" => "example.event", "source" => "/example", "id" => "123"})
{:ok, %Jido.Signal{type: "example.event", source: "/example", id: "123", ...}}

get_extension(signal, namespace)

@spec get_extension(t(), String.t()) :: term() | nil

Retrieves extension data from a Signal.

Returns the extension data stored under the given namespace, or nil if no data exists for that extension.

Parameters

  • signal - The Signal struct to retrieve from
  • namespace - The extension namespace (string)

Returns

The extension data if present, nil otherwise

Examples

# Retrieve authentication data
auth_data = Jido.Signal.get_extension(signal, "auth")
# => %{user_id: "123", roles: ["user"]}

# Non-existent extension returns nil
missing = Jido.Signal.get_extension(signal, "nonexistent")
# => nil

inflate_extensions(attrs)

@spec inflate_extensions(map()) :: {map(), map()}

Inflates top-level attributes back to extension data.

Takes a map of attributes (typically from deserialization) and extracts extension data by calling each registered extension's from_attrs/1 function. Returns both the extensions map and the remaining attributes with extension data removed.

Parameters

  • attrs - Map of attributes to extract extensions from

Returns

A tuple {extensions_map, remaining_attrs} where:

  • extensions_map contains extension data keyed by namespace
  • remaining_attrs has extension-specific attributes removed

Examples

attrs = %{"type" => "test", "source" => "/test", "user_id" => "123", "roles" => ["admin"]}
{extensions, remaining} = Jido.Signal.inflate_extensions(attrs)
# => {%{"auth" => %{user_id: "123", roles: ["admin"]}}, %{"type" => "test", "source" => "/test"}}

list_extensions(signal)

@spec list_extensions(t()) :: [String.t()]

Lists all extensions currently present on a Signal.

Returns a list of extension namespace strings for extensions that have data stored in the Signal.

Parameters

  • signal - The Signal struct to inspect

Returns

A list of extension namespace strings

Examples

# Signal with multiple extensions
extensions = Jido.Signal.list_extensions(signal)
# => ["auth", "tracking", "metadata"]

# Signal with no extensions
extensions = Jido.Signal.list_extensions(signal)
# => []

map_to_signal_data(signals, fields \\ [])

@spec map_to_signal_data([struct()], Keyword.t()) :: [t()]
@spec map_to_signal_data(
  struct(),
  Keyword.t()
) :: t()

Converts a struct or list of structs to Signal data format.

This function is useful for converting domain objects to Signal format while preserving their type information through the TypeProvider.

Parameters

  • signals: A struct or list of structs to convert
  • fields: Additional fields to include (currently unused)

Returns

Signal struct or list of Signal structs with the original data as payload

Examples

# Converting a single struct
iex> user = %User{id: 1, name: "John"}
iex> signal = Jido.Signal.map_to_signal_data(user)
iex> signal.data
%User{id: 1, name: "John"}

# Converting multiple structs
iex> users = [%User{id: 1}, %User{id: 2}]
iex> signals = Jido.Signal.map_to_signal_data(users)
iex> length(signals)
2

new(attrs)

@spec new(map() | keyword()) :: {:ok, t()} | {:error, String.t()}

Creates a new Signal struct.

Parameters

  • attrs: A map or keyword list containing the Signal attributes.

Returns

{:ok, Signal.t()} if the attributes are valid, {:error, String.t()} otherwise.

Examples

iex> Jido.Signal.new(%{type: "example.event", source: "/example", id: "123"})
{:ok, %Jido.Signal{type: "example.event", source: "/example", id: "123", ...}}

iex> Jido.Signal.new(type: "example.event", source: "/example")
{:ok, %Jido.Signal{type: "example.event", source: "/example", ...}}

new(type, data, attrs \\ %{})

@spec new(String.t(), term(), map() | keyword()) :: {:ok, t()} | {:error, String.t()}

Creates a Signal with explicit type and payload, plus optional attrs.

  • type must be a string
  • data can be any term (map, string, etc.)
  • attrs is a map or keyword list for other fields (e.g., :source, :subject, :jido_dispatch)
  • attrs must NOT include :type/"type" or :data/"data"

Examples:

iex> {:ok, s} = Jido.Signal.new("user.created", %{user_id: "123"}, source: "/auth")
iex> s.type
"user.created"
iex> {:ok, s} = Jido.Signal.new("log.message", "Hello world")
iex> s.data
"Hello world"

new!(attrs)

@spec new!(map() | keyword()) :: t() | no_return()

Creates a new Signal struct, raising an error if invalid.

Parameters

  • attrs: A map or keyword list containing the Signal attributes.

Returns

Signal.t() if the attributes are valid.

Raises

RuntimeError if the attributes are invalid.

Examples

iex> Jido.Signal.new!(%{type: "example.event", source: "/example"})
%Jido.Signal{type: "example.event", source: "/example", ...}

iex> Jido.Signal.new!(type: "example.event", source: "/example")
%Jido.Signal{type: "example.event", source: "/example", ...}

new!(type, data, attrs \\ %{})

@spec new!(String.t(), term(), map() | keyword()) :: t() | no_return()

Creates a new Signal struct with explicit type and data, raising an error if invalid.

Parameters

  • type: A string representing the event type (e.g., "user.created").
  • data: The payload (any term; see CloudEvents rules below).
  • attrs: (Optional) A map or keyword list of additional Signal attributes (e.g., :source, :subject).

Returns

Signal.t() if the attributes are valid.

Raises

ArgumentError if the attributes are invalid.

Examples

iex> Jido.Signal.new!("user.created", %{user_id: "123"}, source: "/auth")
%Jido.Signal{type: "user.created", source: "/auth", data: %{user_id: "123"}, ...}

iex> Jido.Signal.new!("user.created", %{user_id: "123"})
%Jido.Signal{type: "user.created", source: "...", data: %{user_id: "123"}, ...}

put_extension(signal, namespace, data)

@spec put_extension(t(), String.t(), term()) :: {:ok, t()} | {:error, String.t()}

Adds extension data to a Signal.

Validates the extension data against the extension's schema and stores it in the Signal's extensions map under the extension's namespace.

Parameters

  • signal - The Signal struct to add the extension to
  • namespace - The extension namespace (string)
  • data - The extension data to add

Returns

{:ok, Signal.t()} if successful, {:error, reason} if validation fails

Examples

# Add authentication extension data
{:ok, signal} = Jido.Signal.put_extension(signal, "auth", %{user_id: "123"})

# Validation will occur based on extension schema
{:ok, signal} = Jido.Signal.put_extension(signal, "tracking", %{session_id: "abc"})

schema()

Returns the Zoi schema for Signal

serialize(signal_or_list, opts \\ [])

@spec serialize(
  t() | [t()],
  keyword()
) :: {:ok, binary()} | {:error, term()}

Serializes a Signal or a list of Signals using the specified or default serializer.

Parameters

  • signal_or_list: A Signal struct or list of Signal structs
  • opts: Optional configuration including:
    • :serializer - The serializer module to use (defaults to configured serializer)

Returns

{:ok, binary} on success, {:error, reason} on failure

Examples

iex> signal = %Jido.Signal{type: "example.event", source: "/example"}
iex> {:ok, binary} = Jido.Signal.serialize(signal)
iex> is_binary(binary)
true

# Using a specific serializer
iex> {:ok, binary} = Jido.Signal.serialize(signal, serializer: Jido.Signal.Serialization.ErlangTermSerializer)
iex> is_binary(binary)
true

# Serializing multiple Signals
iex> signals = [
...>   %Jido.Signal{type: "event1", source: "/ex1"},
...>   %Jido.Signal{type: "event2", source: "/ex2"}
...> ]
iex> {:ok, binary} = Jido.Signal.serialize(signals)
iex> is_binary(binary)
true

serialize!(signal_or_list, opts \\ [])

@spec serialize!(
  t() | [t()],
  keyword()
) :: binary()

Legacy serialize function that returns binary directly (for backward compatibility).