Engagement Engine¶
The Engagement Engine is AMP's inbound counterpart to the Content Pipeline. While the Content Pipeline handles outbound (Strategy → Content → Publish → Analytics → Optimize), the Engagement Engine handles inbound social signals through five distinct layers.
Overview¶
AMP Platform
├── Content Engine (Outbound)
│ └── Strategy → Content → Publish → Analytics → Optimize
│
└── Engagement Engine (Inbound)
└── Sense → Understand → Decide → Act → Learn
The Engagement Engine transforms social signals into intelligent, automated responses while maintaining human oversight where needed.
Architecture¶
┌─────────────────────────────────────────────────────────────────────────┐
│ SENSING LAYER │
│ Mentions │ Comments │ DMs │ Ad Comments │ Keywords │ Competitor Signals │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ UNDERSTANDING LAYER │
│ Intent │ Sentiment │ Urgency │ Account Match │ Revenue │ Risk Flags │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ DECISION LAYER │
│ Policy Engine + Scoring → Respond │ Route │ Create Opp │ Suppress │ Adj│
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ ACTION LAYER │
│ Reply │ DM │ Task Create │ CRM Update │ Ad Creative/Budget Adjustment │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ LEARNING LAYER │
│ Outcome Tracking │ Playbook Updates │ Threshold Calibration │
└─────────────────────────────────────────────────────────────────────────┘
Layer Details¶
1. Sensing Layer¶
The Sensing Layer captures social signals from all connected platforms.
Signal Sources:
| Source | Description | Collection Method |
|---|---|---|
| Mentions | Direct @ mentions | Pinchtab + API webhooks |
| Comments | Replies to posts | Pinchtab polling |
| DMs | Direct messages | Pinchtab (primary) |
| Ad Comments | Comments on paid posts | Pinchtab + Meta API |
| Brand Keywords | Non-@ brand mentions | Pinchtab search monitoring |
| Competitor Signals | Competitor mentions/content | Pinchtab monitoring |
Signal Schema:
type Signal struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
Platform Platform `json:"platform"`
SignalType SignalType `json:"signal_type"`
MessageID string `json:"message_id"`
AuthorID string `json:"author_id"`
AuthorHandle string `json:"author_handle"`
AuthorName string `json:"author_name"`
Content string `json:"content"`
ParentID *string `json:"parent_id"`
ThreadID *string `json:"thread_id"`
URL string `json:"url"`
MediaURLs []string `json:"media_urls"`
Timestamp time.Time `json:"timestamp"`
ReceivedAt time.Time `json:"received_at"`
RawPayload json.RawMessage `json:"raw_payload"`
}
Deduplication:
Signals are deduplicated using composite key: tenant_id + platform + message_id
func (s *SensingService) IsDuplicate(ctx context.Context, signal *Signal) bool {
key := fmt.Sprintf("signal:seen:%s:%s:%s", signal.TenantID, signal.Platform, signal.MessageID)
// Check Redis (fast) then PostgreSQL (durable)
}
2. Understanding Layer¶
The Understanding Layer enriches raw signals with AI-derived classifications.
Enrichments:
| Field | Method | Output |
|---|---|---|
| Intent | LLM Classification | question, complaint, praise, purchase_intent, support_request, spam, other |
| Sentiment | LLM + Rules | Float -1.0 (negative) to +1.0 (positive) |
| Urgency | LLM + Rules | critical, high, normal, low |
| Account Match | CRM Lookup | existing_customer, prospect, vip, competitor, unknown |
| Revenue Potential | Scoring Model | Estimated value or tier |
| Risk Flags | LLM + Rules | legal_mention, influencer, journalist, competitor, profanity |
Enriched Signal Schema:
type EnrichedSignal struct {
Signal
// Understanding enrichments
Intent Intent `json:"intent"`
Sentiment float64 `json:"sentiment"`
Urgency Urgency `json:"urgency"`
AccountMatch *AccountMatch `json:"account_match"`
RevenueScore float64 `json:"revenue_score"`
RiskFlags []RiskFlag `json:"risk_flags"`
Topics []string `json:"topics"`
Language string `json:"language"`
// Processing metadata
EnrichedAt time.Time `json:"enriched_at"`
EnrichmentModel string `json:"enrichment_model"`
Confidence float64 `json:"confidence"`
}
Classification Prompt Template:
Analyze this social media message and classify:
Platform: {platform}
Author: {author_handle}
Content: {content}
Context: {thread_context}
Respond with JSON:
{
"intent": "question|complaint|praise|purchase_intent|support_request|spam|other",
"sentiment": -1.0 to 1.0,
"urgency": "critical|high|normal|low",
"risk_flags": ["legal_mention", "influencer", ...],
"topics": ["topic1", "topic2"],
"reasoning": "brief explanation"
}
3. Decision Layer¶
The Decision Layer applies business rules to determine actions.
Decision Outcomes:
| Outcome | Description |
|---|---|
respond | Auto-generate and send response |
route | Escalate to human with context |
create_opportunity | Create CRM deal/opportunity |
suppress | Log but take no action |
trigger_adjustment | Signal content/ad pipeline to adjust |
Policy Engine:
Policies are evaluated in priority order. First matching policy wins.
policies:
# High-risk always routes to human
- name: "High Risk Escalation"
priority: 100
conditions:
risk_flags:
contains_any: [legal_mention, journalist, influencer]
action: route
route_to: pr_team
sla_minutes: 30
# VIP complaints get fast-tracked
- name: "VIP Complaint"
priority: 90
conditions:
account_match: vip
sentiment: { lt: -0.3 }
intent: complaint
action: route
route_to: customer_success
sla_minutes: 15
# Purchase intent creates opportunity
- name: "Purchase Intent"
priority: 80
conditions:
intent: purchase_intent
risk_flags: { empty: true }
action: respond
response_template: purchase_interest
also:
- create_opportunity
# Praise gets auto-response
- name: "Auto Thank"
priority: 50
conditions:
intent: praise
sentiment: { gt: 0.5 }
risk_flags: { empty: true }
action: respond
response_template: thank_you
# Questions route for draft review
- name: "Question Draft"
priority: 40
conditions:
intent: question
action: respond
require_approval: true
# Spam suppressed
- name: "Suppress Spam"
priority: 10
conditions:
intent: spam
action: suppress
reason: "Classified as spam"
# Default: route to human
- name: "Default Route"
priority: 0
conditions: {}
action: route
route_to: social_team
Policy Schema:
type Policy struct {
ID string `json:"id"`
Name string `json:"name"`
Priority int `json:"priority"`
Conditions PolicyConditions `json:"conditions"`
Action ActionType `json:"action"`
ActionConfig map[string]interface{} `json:"action_config"`
AlsoActions []ActionType `json:"also_actions"`
Enabled bool `json:"enabled"`
}
type PolicyConditions struct {
Intent *Intent `json:"intent,omitempty"`
Sentiment *NumericCondition `json:"sentiment,omitempty"`
Urgency *Urgency `json:"urgency,omitempty"`
AccountMatch *AccountMatchType `json:"account_match,omitempty"`
RiskFlags *ArrayCondition `json:"risk_flags,omitempty"`
Platform *Platform `json:"platform,omitempty"`
SignalType *SignalType `json:"signal_type,omitempty"`
}
Decision Record:
Every decision is logged for auditability and learning.
type Decision struct {
ID string `json:"id"`
SignalID string `json:"signal_id"`
PolicyID string `json:"policy_id"`
PolicyName string `json:"policy_name"`
Action ActionType `json:"action"`
ActionConfig json.RawMessage `json:"action_config"`
AlsoActions []ActionType `json:"also_actions"`
DecidedAt time.Time `json:"decided_at"`
}
4. Action Layer¶
The Action Layer executes decisions across channels.
Action Types:
| Action | Execution | Targets |
|---|---|---|
| Reply | Pinchtab (native) or Ayrshare (API) | Same thread/post |
| DM | Pinchtab | Direct to author |
| Task Create | Internal queue or external (Linear, Asana) | Team assignment |
| CRM Update | HubSpot/Salesforce API | Contact/Deal records |
| Ad Adjustment | Meta/Google Ads API | Budget, creative, targeting |
| Content Trigger | Internal pipeline job | New content generation |
Response Generation:
type ResponseRequest struct {
Signal *EnrichedSignal `json:"signal"`
Template string `json:"template"`
BrandContext *BrandContext `json:"brand_context"`
ThreadHistory []Signal `json:"thread_history"`
MaxLength int `json:"max_length"`
RequireApproval bool `json:"require_approval"`
}
type GeneratedResponse struct {
ID string `json:"id"`
SignalID string `json:"signal_id"`
Content string `json:"content"`
MediaURLs []string `json:"media_urls"`
Status string `json:"status"` // draft, approved, sent, failed
GeneratedAt time.Time `json:"generated_at"`
ApprovedAt *time.Time `json:"approved_at"`
ApprovedBy *string `json:"approved_by"`
SentAt *time.Time `json:"sent_at"`
PlatformID *string `json:"platform_id"` // ID of sent message
}
Execution Flow:
sequenceDiagram
participant Decision
participant ActionQueue
participant Executor
participant Pinchtab
participant Ayrshare
participant CRM
Decision->>ActionQueue: Queue action
ActionQueue->>Executor: Dequeue
alt Response Action
alt Approval Required
Executor->>Executor: Generate draft
Executor->>Executor: Await approval
end
alt Native Execution
Executor->>Pinchtab: Execute via browser
else API Execution
Executor->>Ayrshare: Execute via API
end
else CRM Action
Executor->>CRM: Update record
end
Executor->>ActionQueue: Mark complete 5. Learning Layer¶
The Learning Layer measures outcomes and improves the system.
Tracked Outcomes:
| Metric | Description | Measurement |
|---|---|---|
| Response Rate | Did they reply to us? | Track thread activity |
| Sentiment Shift | Did sentiment improve? | Re-analyze follow-ups |
| Conversion | Did opportunity close? | CRM outcome tracking |
| Escalation Rate | How often auto-responses fail? | Human takeover rate |
| Time to Response | Meeting SLAs? | Timestamp deltas |
| Engagement Lift | Did our reply boost post? | Engagement metrics |
Feedback Schema:
type Outcome struct {
ID string `json:"id"`
DecisionID string `json:"decision_id"`
SignalID string `json:"signal_id"`
ResponseID *string `json:"response_id"`
// Outcome metrics
GotReply bool `json:"got_reply"`
ReplySignalID *string `json:"reply_signal_id"`
SentimentDelta *float64 `json:"sentiment_delta"`
Converted *bool `json:"converted"`
ConversionValue *float64 `json:"conversion_value"`
Escalated bool `json:"escalated"`
TimeToResponse *time.Duration `json:"time_to_response"`
// Timestamps
MeasuredAt time.Time `json:"measured_at"`
}
Learning Loops:
- Template Effectiveness — Track which response templates get positive follow-ups
- Policy Tuning — Adjust thresholds based on escalation/success rates
- Classification Calibration — Compare predictions to human labels
- Content Feedback — Surface common questions to Content Pipeline for proactive content
type PlaybookUpdate struct {
ID string `json:"id"`
UpdateType string `json:"update_type"` // threshold, template, policy
Target string `json:"target"` // what's being updated
OldValue string `json:"old_value"`
NewValue string `json:"new_value"`
Reason string `json:"reason"`
TriggeredBy string `json:"triggered_by"` // metric that triggered
AppliedAt time.Time `json:"applied_at"`
}
Data Flow¶
sequenceDiagram
participant Pinchtab
participant Sensing
participant Understanding
participant Decision
participant Action
participant Learning
Pinchtab->>Sensing: Raw signal
Sensing->>Sensing: Deduplicate
Sensing->>Understanding: Signal
Understanding->>Understanding: Classify intent
Understanding->>Understanding: Score sentiment
Understanding->>Understanding: Check account match
Understanding->>Understanding: Flag risks
Understanding->>Decision: Enriched signal
Decision->>Decision: Evaluate policies
Decision->>Decision: Log decision
Decision->>Action: Action + config
Action->>Action: Generate response (if needed)
Action->>Pinchtab: Execute
Action->>Learning: Log execution
Learning->>Learning: Track outcome
Learning->>Decision: Update thresholds NATS Topics¶
amp.engagement.signals.raw # From Pinchtab/webhooks
amp.engagement.signals.enriched # After Understanding
amp.engagement.decisions # Decision records
amp.engagement.actions.pending # Awaiting execution
amp.engagement.actions.approval # Needs human approval
amp.engagement.actions.executed # Completed actions
amp.engagement.outcomes # Measured outcomes
amp.engagement.learning.updates # Playbook changes
Database Tables¶
-- Signals
CREATE TABLE engagement_signals (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
platform TEXT NOT NULL,
signal_type TEXT NOT NULL,
message_id TEXT NOT NULL,
author_id TEXT,
author_handle TEXT,
author_name TEXT,
content TEXT,
parent_id TEXT,
thread_id TEXT,
url TEXT,
media_urls JSONB,
timestamp TIMESTAMPTZ,
received_at TIMESTAMPTZ NOT NULL,
raw_payload JSONB,
-- Enrichments
intent TEXT,
sentiment FLOAT,
urgency TEXT,
account_match_type TEXT,
account_match_id UUID,
revenue_score FLOAT,
risk_flags TEXT[],
topics TEXT[],
language TEXT,
enriched_at TIMESTAMPTZ,
enrichment_model TEXT,
confidence FLOAT,
UNIQUE(tenant_id, platform, message_id)
);
-- Decisions
CREATE TABLE engagement_decisions (
id UUID PRIMARY KEY,
signal_id UUID NOT NULL REFERENCES engagement_signals(id),
policy_id TEXT NOT NULL,
policy_name TEXT NOT NULL,
action TEXT NOT NULL,
action_config JSONB,
also_actions TEXT[],
decided_at TIMESTAMPTZ NOT NULL
);
-- Responses
CREATE TABLE engagement_responses (
id UUID PRIMARY KEY,
signal_id UUID NOT NULL REFERENCES engagement_signals(id),
decision_id UUID NOT NULL REFERENCES engagement_decisions(id),
content TEXT NOT NULL,
media_urls TEXT[],
status TEXT NOT NULL, -- draft, pending_approval, approved, sent, failed
generated_at TIMESTAMPTZ NOT NULL,
approved_at TIMESTAMPTZ,
approved_by UUID,
sent_at TIMESTAMPTZ,
platform_id TEXT,
error_message TEXT
);
-- Outcomes
CREATE TABLE engagement_outcomes (
id UUID PRIMARY KEY,
decision_id UUID NOT NULL REFERENCES engagement_decisions(id),
signal_id UUID NOT NULL REFERENCES engagement_signals(id),
response_id UUID REFERENCES engagement_responses(id),
got_reply BOOLEAN,
reply_signal_id UUID,
sentiment_delta FLOAT,
converted BOOLEAN,
conversion_value FLOAT,
escalated BOOLEAN,
time_to_response_ms BIGINT,
measured_at TIMESTAMPTZ NOT NULL
);
-- Policies
CREATE TABLE engagement_policies (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
priority INT NOT NULL,
conditions JSONB NOT NULL,
action TEXT NOT NULL,
action_config JSONB,
also_actions TEXT[],
enabled BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
Integration Points¶
With Twenty CRM¶
Twenty CRM serves as the central data hub for customer/prospect data.
Data Flow:
sequenceDiagram
participant Signal
participant Understanding
participant Twenty
participant Decision
participant Action
participant Learning
Signal->>Understanding: Raw signal
Understanding->>Twenty: Lookup author (GraphQL)
Twenty-->>Understanding: Contact/Company data
Understanding->>Understanding: Enrich with account match
Understanding->>Decision: Enriched signal
Decision->>Action: create_opportunity
Action->>Twenty: POST /rest/opportunities
Twenty-->>Action: Opportunity created
Note over Twenty,Learning: Later...
Twenty->>Learning: Webhook: opportunity.won
Learning->>Learning: Update outcome.converted = true Twenty Objects Used:
| Twenty Object | AMP Usage |
|---|---|
People | Social authors, contacts |
Companies | Author's organization (from LinkedIn, etc.) |
Opportunities | Deals created from purchase intent signals |
Tasks | Human follow-up tasks from routed signals |
Notes | Signal context attached to contacts |
Account Matching (Understanding Layer):
// Query Twenty to match social author to existing contact
func (u *UnderstandingService) MatchAccount(ctx context.Context, signal *Signal) (*AccountMatch, error) {
// Search by social handle
query := `
query FindPerson($handle: String!, $platform: String!) {
people(filter: {
or: [
{ twitterHandle: { eq: $handle } }
{ linkedinHandle: { eq: $handle } }
{ instagramHandle: { eq: $handle } }
]
}) {
edges {
node {
id
name
email
company { id name }
isVIP: customFields(name: "is_vip")
lifetimeValue: customFields(name: "ltv")
}
}
}
}
`
// Returns AccountMatch with type (vip, existing_customer, prospect, unknown)
}
Opportunity Creation (Action Layer):
// Create opportunity when purchase intent detected
func (a *ActionService) CreateOpportunity(ctx context.Context, signal *EnrichedSignal) error {
opp := map[string]interface{}{
"name": fmt.Sprintf("Inbound: %s via %s", signal.AuthorHandle, signal.Platform),
"stage": "NEW",
"personId": signal.AccountMatch.AccountID,
"source": string(signal.Platform),
"amount": signal.RevenueScore,
"customFields": map[string]interface{}{
"signal_id": signal.ID,
"signal_type": signal.SignalType,
"intent": signal.Intent,
"original_url": signal.URL,
},
}
return a.twenty.CreateOpportunity(ctx, opp)
}
Webhook Handlers (Learning Layer):
// Handle Twenty webhooks for outcome tracking
func (l *LearningService) HandleTwentyWebhook(ctx context.Context, event TwentyWebhookEvent) error {
switch event.Type {
case "opportunity.updated":
if event.Data.Stage == "WON" {
// Find original signal from custom field
signalID := event.Data.CustomFields["signal_id"]
return l.RecordConversion(ctx, signalID, event.Data.Amount)
}
case "task.completed":
// Track human follow-up completion
return l.RecordTaskCompletion(ctx, event.Data.ID)
}
return nil
}
Twenty Custom Fields for AMP:
Add these custom fields to Twenty objects:
| Object | Field | Type | Purpose |
|---|---|---|---|
| People | twitter_handle | Text | Twitter/X username |
| People | linkedin_handle | Text | LinkedIn username |
| People | instagram_handle | Text | Instagram username |
| People | tiktok_handle | Text | TikTok username |
| People | is_vip | Boolean | VIP flag for priority routing |
| People | ltv | Currency | Lifetime value for scoring |
| Opportunities | signal_id | Text | Link back to engagement signal |
| Opportunities | signal_type | Text | Type of signal that created opp |
| Opportunities | original_url | URL | Link to original social post |
| Tasks | signal_id | Text | Link to routed signal |
| Tasks | urgency | Select | From signal urgency classification |
With Content Pipeline¶
The Engagement Engine feeds back to the Content Pipeline:
- Common Questions → Generate FAQ content
- Product Feedback → Inform content strategy
- Trending Topics → Real-time content opportunities
- Competitor Mentions → Competitive content response
With External Systems¶
| System | Integration | Purpose |
|---|---|---|
| HubSpot/Salesforce | API | Create/update contacts, deals |
| Linear/Asana | API | Create tasks for human follow-up |
| Slack | Webhook | Real-time alerts for critical signals |
| Meta Ads | API | Adjust ad spend based on engagement |
| Google Ads | API | Adjust campaigns based on signals |
Automation Levels¶
Tenants can configure their automation level:
| Level | Sensing | Understanding | Decision | Action |
|---|---|---|---|---|
| 0 - Monitor | Auto | Auto | Log only | None |
| 1 - Triage | Auto | Auto | Auto | Route all |
| 2 - Draft | Auto | Auto | Auto | Draft, human approves |
| 3 - Selective | Auto | Auto | Auto | Auto low-risk, route high |
| 4 - Full Auto | Auto | Auto | Auto | Auto all except critical |
Security Considerations¶
- Session Isolation — Each tenant's Pinchtab sessions are isolated
- Credential Storage — Social credentials encrypted at rest
- Audit Trail — All decisions and actions logged
- Rate Limiting — Per-tenant action limits prevent abuse
- Human Oversight — Critical signals always route to humans
- Content Moderation — Generated responses checked before send
Monitoring¶
Key metrics to track:
- Signal ingestion rate (per platform, per tenant)
- Enrichment latency (p50, p95, p99)
- Decision latency
- Action success rate
- Human approval queue depth
- Outcome tracking coverage
- Policy match distribution