Event follow-up agent
This page explains how an event follow-up agent, under delegated user authority, reads attendee lists, interaction records, and CRM notes, combines them with public company context into segmented follow-up drafts, and writes the drafts back to the CRM as draft records awaiting seller confirmation. After reading it, you will understand which permission boundaries this scenario needs, why the CRM is the canonical source for contact facts, and why GUMem only holds seller-confirmed tone preferences.
Use case
Marketing and sales teams need to quickly organize attendees after webinars, conferences, or in-person events and create personalized follow-up. Attendee lists are scattered across event platforms and the CRM, and researching each company before writing follow-up often drags past the point where the event is still fresh — while handing a script full CRM read-write access carries far more risk than the job requires.
Typical triggers:
- Within 24 hours after a webinar, the first follow-up round must go out segmented by attendee engagement.
- A trade show produces a batch of business cards and badge scans that need company context before handoff to sales.
- A quarterly event review needs to reconcile which attendees already exist in the CRM and which are new contacts.
Engineering challenges
- The freshness window: event momentum decays within 24–48 hours, and manual company research cannot keep up; but skipping fact verification for speed makes personalized content wrong — and wrong personalization damages the relationship.
- The fact boundary of personalization: a line like "congrats on your Series B" must be backed by a public source; unsourced speculation, once sent, is worse than a template email.
- CRM data sovereignty: the single source of truth for segments, event history, and contact facts is the CRM. Copying them into a second store drifts immediately — the Agent's output must go back to the CRM as draft records, promoted to official data only after seller confirmation.
Module composition
| Module | Role | Notes |
|---|---|---|
| GenAuth | Core | Authorized reads of the event platform and CRM: interactive delegation, read-only lists and notes, short-lived revocable credentials, and no send or official-field-write capability. |
| Web Agent | Core | Signs in to the event platform or CRM to read the list and engagement (Profiles reuses login state), and fills in public company context with WebSearch, keeping a source per fact. |
| GUMem | Optional | Only stores seller-confirmed long-term tone preferences (for example, "no exclamation marks in follow-ups"). Segments, event history, and contact facts stay canonical in the CRM — no second source of truth in Memory. |
Permission and delegation boundaries
The Agent holds no inherent permissions. The effective authority for each follow-up task is the intersection of three sets: what the user actually holds ∩ what was explicitly delegated ∩ what the enterprise has approved. Applied to this scenario:
- The delegated scope covers only "read the current event's attendee list, interaction records, and related CRM notes" — no sending email, changing official CRM stages, or editing contact fields.
- Event platform and CRM sign-in are high-risk operations and must use
mode: 'interactive': the user approves in Qoni Console, and the credential is exchanged in a server-side callback. - Attendee personal information is limited to what is publicly visible; the delegation does not cover customer-privacy records beyond the list.
- Out-of-scope attempts (for example, reading another event's list or rewriting official CRM records) are rejected and recorded — the audit chain covers all attempts.
Note: the SDK example requests product-level scopes (such as webagent.do_anything:read). Fine-grained boundaries — platform domains and event scope — are enforced by the GenAuth Agent Profile or your policy layer, not by the task prompt; that configuration is not shown on this page. See Delegate token and attenuation for the full semantics.
Workflow
The user selects an event and a follow-up goal (for example, first-round thanks or sales-lead handoff).
Your app starts interactive delegation; the user approves in Qoni Console, and the server-side callback exchanges the credential.
Your app loads the current version of the follow-up playbook (segmentation criteria, brand voice, follow-up rules) from your policy store and injects it into the task.
Web Agent signs in to the event platform and CRM to read the attendee list, interaction records, and related notes — the CRM is the canonical source for these facts.
Checkpoint: When a login wall, CAPTCHA, or risk-control page appears, the Web Agent should escalate to a human instead of silently bypassing it.
Web Agent researches each attendee's company through WebSearch, collecting only publicly visible information and keeping source URLs.
The Agent segments attendees by engagement and drafts one follow-up per segment, with every company fact carrying its source.
Your app validates the drafts (unsourced facts are cut) and writes them back to the CRM as draft records, with a source list and audit id, awaiting seller confirmation.
Checkpoint: Every company fact in a draft should trace back to a public source; personalized content without evidence should not enter the draft.
Example code
The example below wires this scenario in with the official Qoni SDK (@qoniai/qoni): interactive delegation (full callback) → load the versioned follow-up playbook from your policy store → one doAnything.run() to organize the list and generate drafts → app-side validation, then write back to the CRM as draft records.
import { Qoni, QoniScopes } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
// 1. Entry point: start interactive delegation — event platform and CRM
// sign-in are involved, so the user approves in Qoni Console
export async function startFollowUp(userId: string, eventId: string) {
const { data: authorization } = await qoni.delegateToken({
mode: 'interactive',
agent: 'event-follow-up',
scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
redirectUri: 'https://app.example.com/qoni/callback',
state: eventId, // task params travel to the callback via state; with more
// params, store them in your app and put only a task ID here
user: { id: userId },
expiresIn: 900, // minute-level validity for a single organizing task
})
redirectUserTo(authorization.authorizationUrl)
}
// 2. After the user approves, exchange the delegation token in the
// server-side callback and continue the task
export async function handleQoniCallback(request: Request) {
const query = new URL(request.url).searchParams
const { data: grant } = await qoni.completeDelegateToken({
grantId: query.get('grantId')!,
code: query.get('code')!,
state: query.get('state')!,
})
const eventId = query.get('state')! // the eventId startFollowUp passed via state
return draftEventFollowUps(grant, eventId) // grant.token stays server-side only
}
export async function draftEventFollowUps(
grant: { token: string; auditId: string; grantedScopes: string[] },
eventId: string,
) {
// 3. Before the run: load the current follow-up playbook from YOUR policy store (not Memory)
const playbook = await loadFollowUpPlaybook() // e.g. { version: '2026-08', segments: [...], voice: ... }
// 4. One call organizes the list: read the roster and CRM notes,
// add public context, segment, and draft
const run = await qoni.doAnything.run({
token: grant.token,
prompt: `
Read the attendee list and interaction records for event ${eventId},
plus related CRM notes — the CRM is the canonical source for contact
facts. Add public company context for each attendee, keeping the
source URL for every fact and using only publicly visible personal
information. Segment attendees by engagement and draft one follow-up
per segment. Return drafts as a JSON array of
{ segment, attendeeIds, draft, facts: [{ claim, sourceUrl }] } objects.
Drafts only: never send email or messages, and never change official
CRM stages or contact fields.
Follow-up playbook (version ${playbook.version}):
${JSON.stringify(playbook)}
`,
capture: { screenshots: true },
})
const result = await run.wait({
// Login walls / MFA / risk-control pages: forward to the user
onInteraction: (interaction) => notifyUserActionRequired(interaction),
})
// 5. App-side validation: unsourced company facts are cut from the drafts
const drafts = parseDrafts(result.output).map((d) => ({
...d,
facts: d.facts.filter((f) => f.sourceUrl),
}))
// 6. Write the drafts back to the CRM as draft records (your app calls the
// CRM API directly), awaiting seller confirmation
await crm.createDraftRecords(eventId, drafts, { playbookVersion: playbook.version })
return {
drafts,
artifacts: result.artifacts,
playbookVersion: playbook.version,
audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
}
}The output structure is a contract set by the task prompt: here it is an array of { segment, attendeeIds, draft, facts }, parsed and validated by parseDrafts on the app side, with unsourced facts cut. The SDK itself returns the generic RunResult (runId, status, output, artifacts, and so on). If a seller settles on a long-term tone preference while confirming drafts, your app writes it to GUMem only after confirmation; this example neither reads nor writes Memory by default.
Data and memory boundaries
This scenario touches four kinds of data; only the last optionally belongs in GUMem:
- Versioned rules: segmentation criteria, brand voice, follow-up rules — managed by version in the follow-up playbook, with every batch of drafts referencing the playbook version.
- Business state: segmentation results, follow-up drafts, company facts and sources — written back to the CRM as draft records; segments, event history, and contact facts stay canonical in the CRM, with no second source of truth.
- Audit records: the behavior chain of interactive authorization, every read, and rejected out-of-scope attempts — maintained by GenAuth.
- User Memory (optional): seller-confirmed long-term tone preferences (for example, "no exclamation marks in follow-ups") — this is where GUMem fits; a one-off event pass neither recalls nor writes back by default.
Failure handling
| Situation | Recommended handling |
|---|---|
| Event platform or CRM login state expires | Suspend the task, notify the user to sign in again, and resume from the checkpoint. |
| Public company context is missing or unreliable | Keep only the in-list information for that contact; do not fill in speculative company background. |
| CRM record request outside the delegated scope | Reject and record it; the attempted access remains visible in the audit chain. |
| A draft fact lacks a source | App-side validation cuts the fact and the deliverable notes how many were cut. |
Production notes
Do not send email or rewrite official CRM fields automatically: drafts go back to the CRM as draft records and become official data only after seller confirmation — sending is always a human responsibility. Attendee personal information is limited to publicly visible parts; personal data beyond the list should not enter drafts, CRM draft records, or Memory.
Next steps
- Read the Quickstart to run the shortest path for Agent identity and delegation.
- Read Authorization and browser sandbox for the security boundaries of controlled sessions.
- Continue with the Competitive intelligence agent for an adjacent research scenario.