Invoking from your app
Tools and resolvers let an agent reach into your app. This is the reverse: how ordinary application code — a controller, a service object, a background job, a state machine — hands an agent something to act on. The entry point is Protege::Gateway.message.
Gateway.message
Protege::Gateway.message(
to: "ops@thescoop.com",
subject: "Low-stock check",
body: "Pistachio is down to 4 tubs. Decide whether to reorder and email the supplier if so."
)This hands Scout (the agent at ops@thescoop.com) a brief and lets it act. It's the same path the dashboard console uses, so the message flows through the normal "an inbound message starts a thread" machinery — the reply harness and the persona's message_resolvers.
When The Scoop's checkout flow flags a problem, a service object can loop Sundae in the same way — no need to load the persona record or build any mail:
class OrderIssueReporter
def report(order)
Protege::Gateway.message(
to: "support@thescoop.com",
subject: "Order #{order.number} flagged at checkout",
body: "Payment was declined twice for #{order.customer.email}. Reach out and offer help."
)
end
endto is an address, not a record
to is resolved through the same lookup inbound mail uses, honouring case and plus-tag subaddressing — so you can pass a literal address or persona.email_address without loading the record first. If no active persona owns the address, Gateway.message raises ArgumentError.
It's fire-and-forget
Gateway.message returns the persisted inbound Message (useful for reference or tracing) — not an LLM response. There's nothing the agent must reply to; it reads the brief, runs inference, and uses its (scoped) tools. The outcome is whatever actions it takes — the mail it chooses to send, the records it updates. If you want the agent to email someone, say so in the body; the agent decides and sends via its send_email tool like any other run.
Because it's the console path, a message whose conversation stays at the console_address never touches SMTP — handy for internal, in-app agent work you don't want leaving the building.
Attachments
You can hand the agent files, and they meet your app where the file already is — no need to build a mail attachment by hand. Each entry may be:
- a stored Active Storage blob or attachment (reused by reference, no re-upload),
- an
ActionDispatch::Http::UploadedFile(a controller upload, passed straight through), or - a plain
{ filename:, content_type:, bytes: }hash (raw or generated bytes, uploaded once).
Protege::Gateway.message(
to: "support@thescoop.com",
subject: "Damaged delivery report",
body: "A customer sent this photo of a melted delivery. Assess and offer a refund if warranted.",
attachments: [params[:photo]] # an uploaded file straight from a controller
)The agent sees them exactly as it sees an emailed attachment — via read_attachment and, for images and PDFs, as vision parts on a multimodal model.
Related
- Gateway — the inbound/outbound edge this rides on.
- Tools — the reverse direction, the agent reaching into your app.
- Configuration — the
console_addressthat keeps a conversation in-app.

