Skip to content

Captures

Chronicle.Captures.Capture provides a DSL for defining Chronicle captures, and Chronicle.Captures provides an idiomatic Elixir API for registering, starting, stopping, deleting, listing, and validating them.

Captures turn data that arrives from outside the event log — an external API, an inbound webhook, or another event store’s public events — into events appended to this event store. The DSL compiles at build time to Capture Declaration Language (CDL) text, the same text the kernel’s Workbench and SaveCapture accept. See the Chronicle documentation’s Capture Declaration Language reference for the full grammar.

defmodule MyApp.Captures.InvoiceCapture do
use Chronicle.Captures.Capture, name: "InvoiceCapture"
api_source("InvoicingApi", route: "/invoices", poll: "10m")
key("id")
append MyApp.Events.InvoiceStatusChanged, when: "status" do
set(:status, "$.status")
end
end
  • use Chronicle.Captures.Capture registers :name (defaults to the module’s last segment) and :id (defaults to a deterministic UUIDv5 derived from the module’s full name, so re-registering the same module updates the same kernel-side capture rather than creating a new one every deploy). Pass start: true to have Chronicle.Captures.Registrar start the capture once it has been saved.
  • Exactly one source is required: api_source/1,2 (polls a named External Service), webhook_source/1 (an inbound webhook path), or events_source/1 (captures from another event store’s public events, given as a list of registered Chronicle.Events.EventType modules).
  • key/1 names the property path used to identify and diff a captured item. For an events source, use "$eventSourceId".
  • append/2,3 declares an event to append when its when: condition matches; set/2 inside its block maps a target field of the event’s struct from a source expression ("$.path" for the current payload, "$context.<property>" for the capture or event context).
append MyApp.Events.CustomerChanged, when: "firstName" do
set(:first_name, "$.firstName")
end
append MyApp.Events.CustomerChanged, when: {:any, ["firstName", "lastName"]} do
set(:first_name, "$.firstName")
end
append MyApp.Events.CustomerActivated, when: {:transition, "status", "inactive", "active"} do
set(:status, "$.status")
end
append MyApp.Events.ItemAdded, when: :added do
set(:line_number, "$.lineNumber")
end
  • a string — fires when that property changes
  • {:any, [...]} / {:all, [...]} — an “or”/“and” of several properties
  • {:transition, property, from, to} — fires when a property moves between two specific values
  • :added / :removed — fires when the item is first seen or disappears
  • {:expression, "template"} — a raw CDL expression
map do
rename(:full_name, :fornavn)
template(:greeting, "Hello, ${fornavn}")
translate(:status, :status, [{"aktiv", "active"}, {"inaktiv", "inactive"}])
split(:full_name, by: ",", into: [:first_name, :last_name])
end
nested :billing_address do
map do
rename(:street, :"fakturaAdresse.gate")
end
append MyApp.Events.BillingAddressChanged, when: "street" do
set(:street, "$.street")
end
end
children :line_items, identified_by: :line_number do
append MyApp.Events.ItemAdded, when: :added do
set(:line_number, "$.lineNumber")
end
end

Register a capture module explicitly with Chronicle.Client:

{Chronicle.Client,
connection_string: "chronicle://localhost:35000",
event_store: "my-store",
captures: [MyApp.Captures.InvoiceCapture]}

Or rely on otp_app: auto-discovery — modules that export __chronicle_capture__/1 are discovered automatically, the same way reactors, reducers, and webhooks are. Either way, Chronicle.Captures.Registrar saves every discoverable capture once the client’s connection reaches the :registered phase, and re-registers on every reconnect:

  • It is idempotent: it compares the compiled declaration against what Chronicle.Captures.list/1 reports and skips a capture whose declaration already matches.
  • It warns, rather than fails, before saving over a capture that is :started and whose declaration changed — saving a started capture stops it on the kernel, so it comes back stopped.
  • It starts a capture only when it declares start: true.
{:ok, capture} = Chronicle.Captures.register(MyApp.Captures.InvoiceCapture)
:ok = Chronicle.Captures.start(MyApp.Captures.InvoiceCapture)
{:ok, captures} = Chronicle.Captures.list()
{:ok, messages} = Chronicle.Captures.validate(MyApp.Captures.InvoiceCapture)
{:ok, events} = Chronicle.Captures.captured_events("InvoiceCapture")
:ok = Chronicle.Captures.stop(MyApp.Captures.InvoiceCapture)
:ok = Chronicle.Captures.delete(MyApp.Captures.InvoiceCapture)

Every function also accepts a raw capture id (a GUID string) instead of a module for start/2, stop/2, and delete/2, and raw CDL text instead of a module for validate/2 — useful for validating a declaration before it is ever saved.

MyApp.Captures.InvoiceCapture.__chronicle_capture__(:id)
MyApp.Captures.InvoiceCapture.__chronicle_capture__(:name)
MyApp.Captures.InvoiceCapture.__chronicle_capture__(:declaration)
MyApp.Captures.InvoiceCapture.__chronicle_capture__(:start?)