ring-using-outbox

star 197

Using the transactional-outbox pattern across lib-streaming (writer) and lib-commons/v5/commons/outbox (repository + relay), in two modes. Sweep Mode detects DIY outbox tables, hand-rolled relay loops, send-and-pray emits, missing WithOutboxTx wrapping, and broker calls inside DB transactions. Reference Mode catalogs the writer/repository/envelope API and relay wiring. Go-only. Skip for non-Go or read-only services.

LerianStudio By LerianStudio schedule Updated 6/6/2026

name: ring:using-outbox description: "Using the transactional-outbox pattern across lib-streaming (writer) and lib-commons/v5/commons/outbox (repository + relay), in two modes. Sweep Mode detects DIY outbox tables, hand-rolled relay loops, send-and-pray emits, missing WithOutboxTx wrapping, and broker calls inside DB transactions. Reference Mode catalogs the writer/repository/envelope API and relay wiring. Go-only. Skip for non-Go or read-only services."

ring:using-outbox

When to use

Sweep mode:

  • "Sweep for transactional outbox violations"
  • "Find send-and-pray emits"
  • "Are we wrapping DB transactions with WithOutboxTx?"
  • "Migrate this service from DIY outbox to lib-streaming + lib-commons/outbox"
  • "Audit relay loops for hand-rolled poller patterns"

Reference mode:

  • "How does the transactional outbox pattern work?"
  • "Which writer interface do I implement for X?"
  • "What goes in OutboxEnvelope?"
  • "How do I wire the relay loop?"
  • "How does WithOutboxTx interact with MongoDB sessions?"

Skip when

  • Working on non-Go services
  • Service has no events to emit (pure read-side, BFF)
  • Working on frontend code

Related

Parent surface: ring:using-lib-streaming (full streaming bus) Repository side: ring:using-lib-commons (lib-commons/outbox dispatcher, repository, handler registry) Adjacent: ring:instrumenting-streaming-events (eventable-point identification → emit wiring), ring:using-runtime (panic-safe relay loops), ring:using-assert (invariant checks on envelope decode)


The Pattern

The transactional outbox solves one operational invariant: business state and the event that announces it must commit atomically, or not at all. Without it, three failure modes are inevitable in production:

  1. Lost event. Business state commits, the producer calls broker.Emit, the broker is down or the network blips — the event vanishes. The ledger now believes a transaction happened that no downstream consumer ever heard about.
  2. Phantom event. Producer emits successfully, then the DB commit fails. Downstream consumers now act on a transaction that never happened.
  3. Send-and-pray. Code paths that emit on a best-effort basis "and we'll log it if it fails" — a polite name for systematic data loss under sustained broker outages.

The outbox pattern fixes this by writing the event to an outbox table inside the same database transaction as the business state. Commit is atomic: either both rows persist or neither does. A separate process — the relay (also called dispatcher or poller) — reads pending outbox rows and publishes them to the broker, marking each row PUBLISHED on success. Delivery becomes at-least-once: if the relay crashes between publish and mark, the row stays pending and the next cycle retries. Consumers must be idempotent — that is the cost of at-least-once.

In lib-streaming the producer also uses the outbox as a circuit-breaker fallback. When a target's circuit is OPEN, Emit writes a route-aware OutboxEnvelope instead of attempting the broker call. When the breaker closes, the relay drains the backlog through the originating target's adapter — bypassing Emit itself, so replays cannot re-enter the circuit and cannot re-enqueue themselves. This is what OutboxModeFallbackOnCircuitOpen (the default) buys you: a broker outage degrades to a write-ahead log instead of dropped events.

Mode Selection

Request Shape Mode
"Sweep / audit / find DIY outbox / send-and-pray" Sweep
"How does the pattern work?" Reference
"Which interface do I implement?" Reference
"How do I wire WithOutboxTx in my repository layer?" Reference
"What is the OutboxEnvelope wire format?" Reference

SWEEP MODE

Dispatch 6 explorers in one parallel batch. Each writes its findings JSON; a synthesizer consolidates.

Phase 1: Outbox surface reconnaissance → outbox-surface.json
Phase 2: Multi-angle DIY sweep         → 6 × outbox-sweep-{N}-{angle}.json
Phase 3: Consolidated report           → outbox-sweep-report.md + outbox-sweep-tasks.json

Phase 1: Surface Reconnaissance

Before sweeping, determine what the service currently does:

  1. Grep for lib-streaming and lib-commons/v5/commons/outbox imports in go.mod / source.
  2. Locate broker-publish call sites (any of: Emit, kafka.Produce, sqs.SendMessage, rabbitmq.Publish, custom wrappers).
  3. Locate DB-transaction boundaries (db.BeginTx, *sql.Tx, repository transactional helpers).
  4. Emit /tmp/outbox-surface.json:
{
  "uses_lib_streaming": true,
  "uses_lib_commons_outbox": true,
  "broker_call_sites": [{"file": "...", "line": 0, "kind": "kafka|sqs|rabbitmq|custom"}],
  "tx_boundaries": [{"file": "...", "line": 0}],
  "has_outbox_table_migration": true,
  "has_relay_loop": false
}

If uses_lib_streaming=false AND broker_call_sites is non-empty → flag as high-risk send-and-pray candidate before angle dispatch.

Phase 2: 6-Angle DIY Sweep

⛔ STOP-CHECK BEFORE DISPATCH

Before emitting any Task call, count the explorers you intend to launch in this turn.

  • Count MUST equal 6.
  • If count < 6 → STOP. Do not partial-dispatch. Reconcile against the 6 angles below and try again.
  • The 6 angles are the canonical sweep. No substitutions, no omissions.

⛔ MUST NOT trickle-dispatch

All 6 explorers leave in the SAME TURN, before reading any explorer output.

Forbidden sequences:

  • Dispatch explorer 1 → read result → dispatch explorer 2
  • Dispatch a subset → wait → dispatch the rest
  • Dispatch follow-up explorers conditioned on partial output
  • Loop sequentially over the angle list

If you find yourself about to dispatch an explorer in a turn AFTER any explorer has already returned a result → STOP. You violated parallel dispatch. Report the violation and mark the phase INCOMPLETE rather than completing the trickle.

Self-verify after dispatch

After the dispatch turn, verify all 6 Task calls were emitted in that single turn. If fewer than 6 went out, the phase did NOT execute correctly. Mark INCOMPLETE and surface the dispatch failure — do NOT silently continue with a partial pool.

Parallel dispatch — atomic batch

Emit all 6 Task calls in a SINGLE TURN, as one atomic batch.

If your runtime exposes a multi_tool_use.parallel wrapper, use it to dispatch the complete pool in one wrapped invocation. This is the canonical fan-out mechanism on OpenAI-style tool envelopes and on certain Anthropic SDK consumers — naming it explicitly activates parallel emission on runtimes where trickle-dispatch is the default behavior.

If your runtime emits parallel tool_use blocks natively (Claude Code with Claude models), multi_tool_use.parallel may not be needed — but naming it is harmless and serves as an enforcement anchor.

The STOP-CHECK, anti-trickle, and self-verify guards above remain binding regardless of which mechanism your runtime uses.

Dispatch all 6 in parallel (subagent_type: ring:codebase-explorer). Each writes one JSON file.

Angle 1 — DIY outbox tables without OutboxEnvelope wire format (CRITICAL)

Look for:

  • Tables named outbox, event_outbox, pending_events, tx_outbox, etc. not populated through lib-commons/v5/commons/outbox.OutboxRepository.
  • Custom JSON payload shapes that omit version, route_key, definition_key, target, transport, destination, aggregate_id, requirement, policy, event — the canonical OutboxEnvelope fields.
  • Custom event_type strings that are not "lerian.streaming.publish" (StreamingOutboxEventType).

Replacement: Use outbox.OutboxRepository for persistence; let lib-streaming build the envelope via WithOutboxRepository(repo). The envelope wire format is the authoritative shape — diverging from it makes the row un-replayable by the canonical relay handler.

Evidence to capture: Migration file path + line of the column DDL, and the Go file populating it.

Angle 2 — Hand-rolled relay loops / pollers (CRITICAL)

Look for:

  • for { ... time.Sleep(...) ... SELECT ... FROM outbox WHERE status = 'PENDING' ... } patterns.
  • Hand-rolled SELECT ... FOR UPDATE SKIP LOCKED claim queries.
  • Custom backoff, retry, batch sizing, and dead-event handling logic outside outbox.Dispatcher.
  • Goroutines that publish from a pending table without using outbox.HandlerRegistry.

Replacement: outbox.NewDispatcher(repo, handlers, logger, tracer, opts...) with Producer.RegisterOutboxRelay(handlers) to bind the lerian.streaming.publish handler. Defaults: DispatchInterval=2s, BatchSize=50, MaxDispatchAttempts=10, RetryWindow=5m, ProcessingTimeout=10m.

Why it matters: Hand-rolled relays universally miss one or more of: priority event types, stuck-PROCESSING reclaim, per-tenant fairness, retry classification, observability trident on publish failure. The lib-commons dispatcher gets these right.

Angle 3 — Send-and-pray emits (CRITICAL)

Look for:

  • Broker calls (Emit, Produce, SendMessage, Publish) inside DB-transactional code paths without a preceding streaming.WithOutboxTx(ctx, tx) wrap.
  • The pattern tx.Commit(); broker.Emit(...) — commit succeeds, broker call fails, event lost forever.
  • The pattern broker.Emit(...); tx.Commit() — broker call succeeds, commit fails, phantom event.
  • Comments like // best effort emit, // log if fails, // fire and forget.
  • go func() { broker.Emit(...) }() after a transactional path.

Replacement: Wrap the transaction context: ctx = streaming.WithOutboxTx(ctx, tx); err := emitter.Emit(ctx, req). If the circuit is open, the envelope joins the SQL transaction via TransactionalOutboxWriter.WriteWithTx. If the broker is healthy, direct publish proceeds normally — outbox is the fallback path. Either way, atomicity is preserved.

Severity rationale: Every send-and-pray site is a confirmed data-loss vector. Mark CRITICAL even if "it has never happened in production" — sample size of one outage produces the bug.

Angle 4 — Missing WithOutboxTx wrapping (HIGH)

Look for:

  • Service methods that take *sql.Tx (or wrap one) AND call emitter.Emit without streaming.WithOutboxTx(ctx, tx) first.
  • Repository helpers that begin a transaction internally and emit events without exposing the tx to outbox plumbing.
  • MongoDB session contexts: the same rule applies — emits inside a mongo.SessionContext (v1 driver) ride the session through Write(ctx, ...); emits outside a session lose atomicity. Driver v2 session contexts are not joined by the v1 repository path — flag any v2 session passed to emit as an integration gap.

Replacement: Standardize transactional code paths to:

err := db.RunInTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
    if err := repo.SaveBusinessState(ctx, tx, state); err != nil { return err }
    ctx = streaming.WithOutboxTx(ctx, tx)
    return emitter.Emit(ctx, streaming.EmitRequest{ ... })
})

Diagnostic: If errors.Is(err, streaming.ErrOutboxTxUnsupported) surfaces in logs, the writer is not a TransactionalOutboxWriter — either the wrong writer was wired, or a non-*sql.Tx value sat on txContextKey. Both are wiring bugs, not runtime failures.

Angle 5 — Hand-rolled idempotency keys instead of outbox-provided dedup (MEDIUM)

Look for:

  • Custom processed_events / emitted_events tables tracking which events have been emitted.
  • Code paths that read business state, hash it, and use that hash to skip duplicate emits.
  • Application-layer dedup that exists because the team did not trust the broker path.

Replacement: The outbox row itself is the idempotency key. Once committed in the transaction, it will be published exactly once successfully (the relay marks PUBLISHED on success) and possibly multiple times on consumer side (at-least-once). Consumer-side dedup belongs at the consumer; producer-side dedup tables are usually a workaround for missing outbox.

Subtlety: Distinguish from legitimate consumer idempotency (e.g., commons/idempotency for HTTP request dedup or commons/dlq for retry-queue dedup). Those serve different concerns and are not replaced by the outbox.

Angle 6 — Synchronous broker calls inside DB transactions (HIGH)

Look for:

  • BeginTxbroker.Publish(...)Commit(). The broker call is now inside the transaction; broker latency stretches transaction duration; broker timeout aborts the transaction; broker hang blocks the connection pool slot.
  • Custom code that "pre-flights" the broker before committing the tx, on the theory of "if we can't publish, don't commit."

Replacement: Never call the broker synchronously inside an open transaction. Either:

  • Use WithOutboxTx (envelope joins the tx, broker is decoupled), or
  • Defer the emit to after Commit() and accept the small at-most-once gap (only safe for non-critical events).

Why it matters: Synchronous broker-in-tx is the classic distributed-systems anti-pattern — it couples two failure domains that should be independent and inverts the latency profile of both.

Phase 3: Consolidated Report

Dispatch synthesizer:

Read /tmp/outbox-surface.json and /tmp/outbox-sweep-*.json (6 files).
Emit:
1. /tmp/outbox-sweep-report.md — findings grouped by severity (CRITICAL → MEDIUM)
2. /tmp/outbox-sweep-tasks.json — one task per cluster (same file or repository = one task)

MUST NOT invent findings. MUST cite file:line for every finding. MUST preserve
explorer severity unless evidence contradicts it (justify in notes).

Surface the report path + task count; offer handoff to ring:running-dev-cycle.


REFERENCE MODE

API Catalog

Writer side (github.com/LerianStudio/lib-streaming)

// Minimal durable-write boundary. Custom outbox stores adapt this interface.
type OutboxWriter interface {
    Write(ctx context.Context, envelope OutboxEnvelope) error
}

// Joins an ambient *sql.Tx so the envelope persists inside the caller's
// unit of work. lib-streaming's built-in adapter (libCommonsOutboxWriter)
// implements this when wired via WithOutboxRepository against a SQL repo.
type TransactionalOutboxWriter interface {
    OutboxWriter
    WriteWithTx(ctx context.Context, tx *sql.Tx, envelope OutboxEnvelope) error
}

// Stores an ambient *sql.Tx on ctx for transactional outbox writes.
// The publish path strict-asserts the value type: a non-*sql.Tx on
// txContextKey returns ErrOutboxTxUnsupported (wiring bug).
func WithOutboxTx(ctx context.Context, tx *sql.Tx) context.Context

// Producer options (mutually exclusive — last call wins).
// Mixing them is safe only when the custom writer implements
// TransactionalOutboxWriter; otherwise tx-mode emits fail at runtime.
func WithOutboxRepository(repo outbox.OutboxRepository) EmitterOption
func WithOutboxWriter(writer OutboxWriter) EmitterOption

// Producer method — wires the canonical relay handler under the stable
// event type "lerian.streaming.publish".
func (p *Producer) RegisterOutboxRelay(registry *outbox.HandlerRegistry) error

Repository side (github.com/LerianStudio/lib-commons/v5/commons/outbox)

type OutboxRepository interface {
    Create(ctx context.Context, event *OutboxEvent) (*OutboxEvent, error)
    CreateWithTx(ctx context.Context, tx *sql.Tx, event *OutboxEvent) (*OutboxEvent, error)
    ListPending(ctx context.Context, limit int) ([]*OutboxEvent, error)
    ListPendingByType(ctx context.Context, eventType string, limit int) ([]*OutboxEvent, error)
    ListTenants(ctx context.Context) ([]string, error)
    GetByID(ctx context.Context, id uuid.UUID) (*OutboxEvent, error)
    MarkPublished(ctx context.Context, id uuid.UUID, publishedAt time.Time) error
    MarkFailed(ctx context.Context, id uuid.UUID, errMsg string, maxAttempts int) error
    ListFailedForRetry(ctx context.Context, limit int, before time.Time, maxAttempts int) ([]*OutboxEvent, error)
    ResetForRetry(ctx context.Context, limit int, before time.Time, maxAttempts int) ([]*OutboxEvent, error)
    ResetStuckProcessing(ctx context.Context, limit int, before time.Time, maxAttempts int) ([]*OutboxEvent, error)
    MarkInvalid(ctx context.Context, id uuid.UUID, errMsg string) error
}

// Built-in implementations:
//   github.com/LerianStudio/lib-commons/v5/commons/outbox/postgres
//   github.com/LerianStudio/lib-commons/v5/commons/outbox/mongo

// Handler dispatch.
type EventHandler func(ctx context.Context, event *OutboxEvent) error

type HandlerRegistry struct { /* ... */ }
func NewHandlerRegistry() *HandlerRegistry
func (r *HandlerRegistry) Register(eventType string, handler EventHandler) error
func (r *HandlerRegistry) Handle(ctx context.Context, event *OutboxEvent) error

// Relay loop.
func NewDispatcher(
    repo OutboxRepository,
    handlers *HandlerRegistry,
    logger libLog.Logger,
    tracer trace.Tracer,
    opts ...DispatcherOption,
) (*Dispatcher, error)

func (d *Dispatcher) Run(launcher *libCommons.Launcher) error
func (d *Dispatcher) RunContext(ctx context.Context, launcher *libCommons.Launcher) error
func (d *Dispatcher) Stop()
func (d *Dispatcher) Shutdown(ctx context.Context) error
func (d *Dispatcher) DispatchOnce(ctx context.Context) int                // testing
func (d *Dispatcher) DispatchOnceResult(ctx context.Context) DispatchResult

Wire format (OutboxEnvelope)

const (
    StreamingOutboxEventType = "lerian.streaming.publish"
    OutboxEnvelopeVersion    = 1 // strict equality on decode
)

type OutboxEnvelope struct {
    Version       int              `json:"version"`        // ==1
    RouteKey      string           `json:"route_key"`      // canonical: lower-case dot-delimited
    DefinitionKey string           `json:"definition_key"`
    Target        string           `json:"target"`         // e.g. "kafka-primary"
    Transport     TransportKind    `json:"transport"`      // kafka|sqs|rabbitmq|eventbridge|custom
    Destination   Destination      `json:"destination"`
    AggregateID   uuid.UUID        `json:"aggregate_id"`   // deterministic from event partition key
    Requirement   RouteRequirement `json:"requirement"`    // required|optional
    Policy        DeliveryPolicy   `json:"policy"`
    Event         Event            `json:"event"`
}

The envelope is JSON-marshalled into OutboxEvent.Payload; OutboxEvent.EventType is always "lerian.streaming.publish". The concrete destination lives inside the envelope, not in the row's event type — one outbox table serves every route.

ValidateShape runs on the producer-side persist path (skips DNS/SSRF checks because the destination came from an already-validated RouteDefinition). Validate runs on the relay decode path because persisted bytes are effectively untrusted.

Wiring Recipe

// 1. Build the outbox repository (Postgres example).
outboxRepo, err := outboxpg.NewRepository(db, ...)
if err != nil { return err }

// 2. Build the producer with outbox plumbing.
emitter, err := streaming.NewBuilder().
    Catalog(catalog).
    Routes(routes...).
    Target(streaming.TargetConfig{Name: "kafka-primary", Kind: streaming.TransportKafkaLike, Brokers: cfg.Brokers}).
    Logger(logger).
    Tracer(tracer).
    CircuitBreakerManager(cb).
    OutboxRepository(outboxRepo).   // wires libCommonsOutboxWriter
    Build(ctx)
if err != nil { return err }

// 3. Register the relay handler under the stable event type.
handlers := outbox.NewHandlerRegistry()
producer := emitter.(*streaming.Producer)
if err := producer.RegisterOutboxRelay(handlers); err != nil { return err }

// 4. Start the relay loop.
dispatcher, err := outbox.NewDispatcher(outboxRepo, handlers, logger, tracer)
if err != nil { return err }
if err := launcher.Add("streaming", producer); err != nil { return err }
if err := launcher.Add("outbox-dispatcher", dispatcher); err != nil { return err }

Transactional Emit Pattern

func (s *TransactionService) Post(ctx context.Context, req PostRequest) error {
    return s.db.RunInTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
        // 1. Business state — atomic with the outbox row.
        if err := s.repo.SaveTransaction(ctx, tx, req); err != nil {
            return err
        }

        // 2. Join the SQL tx so circuit-open emits write to outbox under tx.
        ctx = streaming.WithOutboxTx(ctx, tx)

        // 3. Emit. Healthy broker → direct publish (after Commit returns).
        //    Open circuit → envelope persists inside the tx; relay drains later.
        return s.emitter.Emit(ctx, streaming.EmitRequest{
            DefinitionKey: "transaction.posted",
            TenantID:      req.TenantID,
            Subject:       req.TxID,
            Payload:       req.Payload(),
        })
    })
}

Three operationally important properties of this pattern:

  1. Atomicity. Business row and outbox row share a commit fate. No silent divergence.
  2. At-least-once delivery. The relay re-publishes pending rows on every cycle until MarkPublished succeeds. Consumers must dedup.
  3. No re-enqueue under outage. Relay handlers dispatch to the originating target adapter directly (not through Emit), so a sustained circuit-open does not re-add rows to the outbox via the relay path.

Relay Loop Semantics

The dispatcher's collectEvents uses a priority-layered batch (BatchSize=50 default):

  1. Priority events. PriorityEventTypes ∩ PENDING, up to PriorityBudget.
  2. Stuck reclaim. PROCESSING older than ProcessingTimeout=10m → reset to PENDING.
  3. Failed retry. FAILED older than RetryWindow=5m with attempts < MaxDispatchAttempts=10 → reset to PENDING.
  4. Remaining pending. Plain FIFO by created_at.

Per-event publish runs through HandlerRegistry.Handle(ctx, event), dispatched by event.EventType. For streaming outbox rows that means lerian.streaming.publish → the producer's handleOutboxRow, which decodes the OutboxEnvelope (full Validate with SSRF/DNS), looks up the route's transport adapter, and publishes directly through it.

Publish failure is classified via RetryClassifier:

  • Non-retryable → MarkInvalid (poison row, no further attempts).
  • Retryable → MarkFailed with exponential backoff via RetryWindow.

Per-tenant dispatch is round-robin; tenant order rotates each cycle to prevent a slow tenant from starving the rest. Set IncludeTenantMetrics=true on DispatcherConfig only when tenant cardinality is bounded — the dispatcher caps unique labels at MaxTenantMetricDimensions with an overflow bucket (_other).

MongoDB Tx Note

TransactionalOutboxWriter is defined against *sql.Tx. MongoDB transactions flow differently: the v1 driver's mongo.SessionContext rides on ctx and the mongo outbox repository's Create(ctx, ...) picks it up automatically. Callers do not use WithOutboxTx for MongoDB — they invoke Emit with a session-bound context, and the regular Write path joins the Mongo transaction. Driver v2 session contexts are a different type and are not joined by the v1 repository path; mixing v2 sessions with the v1 outbox repository is a wiring bug.

Error Surface

Sentinel Cause Action
streaming.ErrOutboxNotConfigured Policy elected outbox but no writer wired Wire WithOutboxRepository or WithOutboxWriter
streaming.ErrOutboxTxUnsupported WithOutboxTx set, writer is not TransactionalOutboxWriter (or non-*sql.Tx on context key) Use a SQL repository, or remove WithOutboxTx for non-SQL paths
streaming.ErrInvalidOutboxEnvelope Envelope shape rejected on encode or decode Inspect violation field; usually schema-evolution skew during rolling deploy
outbox.ErrHandlerNotRegistered Relay row has unknown event_type Call RegisterOutboxRelay before dispatcher starts
outbox.ErrOutboxRepositoryRequired Dispatcher built with nil repo Wire the repository explicitly

Decision Tree

Situation Use
Service emits events from transactional code WithOutboxRepository + WithOutboxTx + dispatcher
Service emits events outside any DB transaction WithOutboxRepository (no WithOutboxTx needed) — outbox still acts as circuit-breaker fallback
Custom outbox store (Spanner, DynamoDB) Implement OutboxWriter (and TransactionalOutboxWriter if applicable), wire WithOutboxWriter
Read-side service with no emits Skip outbox entirely. Use NoopEmitter for tests.
Internal command queue (request/reply) NOT outbox. Use lib-commons/rabbitmq or lib-commons/dlq per request shape.
HTTP request idempotency NOT outbox. Use lib-commons/idempotency.

Anti-Patterns (closed-form)

  • Outbox tables that store unwrapped event payloads. Without OutboxEnvelope you lose route, target, transport, requirement, and policy — and you cannot use the canonical relay.
  • Relay loops with their own SQL. Hand-rolled FOR UPDATE SKIP LOCKED claim queries that drift from the dispatcher's claim semantics produce stuck rows under load.
  • MarkPublished before broker ack. At-least-once requires marking after the adapter confirms. Reversing the order silently downgrades to at-most-once.
  • Mixing WithOutboxRepository and WithOutboxWriter carelessly. Last call wins. If the custom writer does not implement TransactionalOutboxWriter, every WithOutboxTx emit fails until reverted.
  • Outbox row payloads > DefaultMaxPayloadBytes (1 MiB). Large payloads belong in object storage with a reference in the event, not inline.
Install via CLI
npx skills add https://github.com/LerianStudio/ring --skill ring-using-outbox
Repository Details
star Stars 197
call_split Forks 22
navigation Branch main
article Path SKILL.md
More from Creator
LerianStudio
LerianStudio Explore all skills →