Learning Digest Fan-out

Pollen + Bloom event pattern: a weekly cron emits one signal per learner, each running as that user.

What this demonstrates

The learning example demonstrates the Pollen + Bloom fan-out pattern. A custom WeeklyDigestFanoutProducer fires every Monday at 06:00 UTC, enumerates active learners, and emits one weekly-digest.due signal per (tenant, user) pair. Each signal triggers a Bloom that runs under the addressed_to_user identity — the service account acts on each learner's RAG scope without impersonating them. Because parallelism: per_user is set, all digests run concurrently. Each user only sees their own digest (visibility: addressed).

This example does not include a separate orchid.yml; the event and agent configuration are self-contained in agents.yaml. Wire it into any orchid.yml via agents.config_path.

Run it

Wire agents.yaml into an orchid.yml with your storage and LLM settings:

pip install -e ./orchid -e ./orchid-api
# Point your orchid.yml at examples/learning/agents.yaml
ORCHID_CONFIG=<your-orchid.yml> uvicorn orchid_api.main:app --port 8000

Or drive a single fan-out tick in a test without the scheduler:

pip install -e ./orchid -e ./orchid-cli
# See examples/learning/tests/ for the programmatic producer pattern

Configuration walkthrough

agents.yaml defines the digest agent and the full event pipeline:

# agents.yaml (trimmed)
version: "1"

defaults:
llm:
  model: "ollama/llama3.2"
  temperature: 0.2
rag:
  enabled: false

agents:
digest:
  description: "Personalised weekly learning digest."
  prompt: |
    Build a short personalised digest for the named learner.
    Sections: Highlights from last week, Recommended next steps,
    One stretch goal. Keep it under 150 words.
  rag:
    enabled: false
  execution_hints:
    parallel_safe: true

events:
enabled: true

store:
  class: orchid_ai.events.backends.sqlite.SQLiteEventStorage
  extra_args:
    dsn: /data/chats.db

queue:
  class: orchid_ai.events.queues.sqlite.SQLiteSignalQueue
  poll_interval_ms: 200
  lease_seconds: 30
  max_attempts: 2

producers:
  - class: examples.learning.producers.weekly_digest.WeeklyDigestFanoutProducer
    extra_args:
      cron: "0 6 * * 1"          # every Monday at 06:00 UTC
  - class: orchid_ai.events.producers.internal.InternalEmissionProducer

processors:
  - class: orchid_ai.events.processors.asyncio_pool.AsyncioWorkerPoolProcessor
    concurrency: 4

triggers:
  - id: weekly-digest
    "on": { signal: weekly-digest.due }
    emits:
      agent: digest
      prompt_template: |
        Build a personalised weekly learning digest for user
        {{user_id}} in tenant {{tenant_key}}, week {{payload.week_iso}}.
      identity:
        mode: addressed_to_user
        service_account: digest-bot
        user_id_from: signal.user_id
    retry: { max: 2, backoff: exponential }
    parallelism: per_user

# ...truncated

The digest agent lives in agents/digest.md:

---
description: "Personalised weekly learning digest."
rag:
  enabled: false
execution_hints:
  parallel_safe: true
---

Build a short personalised digest for the named learner.
Sections: Highlights from last week, Recommended next steps,
One stretch goal. Keep it under 150 words.

What to look for

  • producers[0].class: examples.learning.producers.weekly_digest.WeeklyDigestFanoutProducer → a custom OrchidSignalProducer subclass; the framework calls its tick() at each cron interval and it decides how many signals to emit.
  • identity.mode: addressed_to_user → the Bloom runs with the learner's user_id in scope (for RAG scoping) without requiring the user to be logged in.
  • parallelism: per_user → one concurrent Bloom per (tenant, user) pair; the framework ensures two digests for the same user don't overlap.
  • visibility: addressed (implicit default) → each user only sees their own digest via the API; other users in the same tenant cannot access it.
  • concurrency: 4 on the processor → up to four Blooms run in parallel within the process; tune this for your host capacity.

Related concepts