Skip to main content
An automated feedback processing pipeline that analyzes sentiment with AI, categorizes issues, and creates tickets for negative feedback. This example shows how to combine workflows with agents and connectors for a hands-off analysis system.

Project Structure

feedback-analysis/
├── main.ts
├── workflows/
│   └── analyze-feedback.ts
├── agents/
│   ├── sentiment.ts
│   └── action-recommender.ts
├── config/
│   └── settings.ts
├── .runflow/
│   └── rf.json
├── package.json
└── tsconfig.json

Step 1: Configuration

config/settings.ts
export const FEEDBACK_CATEGORIES = [
  'product_bug',
  'feature_request',
  'ux_issue',
  'performance',
  'documentation',
  'pricing',
  'support_quality',
  'praise',
  'other',
] as const;

export type FeedbackCategory = (typeof FEEDBACK_CATEGORIES)[number];

export const SENTIMENT_THRESHOLDS = {
  negative: -0.3, // Below this = negative
  positive: 0.3,  // Above this = positive
};

Step 2: Analysis Agents

Sentiment Analyzer

Cheap, fast model for classification:
agents/sentiment.ts
import { Agent, openai } from '@runflow-ai/sdk';

export const sentimentAgent = new Agent({
  name: 'Sentiment Analyzer',
  instructions: `You analyze customer feedback for sentiment and categorization.

## Task
Analyze the feedback and return a structured analysis.

## Response Format
Respond with valid JSON only:
{
  "sentiment": "positive" | "neutral" | "negative",
  "score": <number between -1.0 and 1.0>,
  "category": "<one of: product_bug, feature_request, ux_issue, performance, documentation, pricing, support_quality, praise, other>",
  "themes": ["<key theme 1>", "<key theme 2>"],
  "summary": "<one sentence summary of the feedback>"
}`,
  model: openai('gpt-4o-mini'),
  modelConfig: { temperature: 0 },
});

Action Recommender

Better model for nuanced recommendations:
agents/action-recommender.ts
import { Agent, openai } from '@runflow-ai/sdk';

export const actionAgent = new Agent({
  name: 'Action Recommender',
  instructions: `You recommend specific actions based on customer feedback analysis.

## Task
Given the original feedback and sentiment analysis, recommend concrete actions.

## Response Format
Respond with valid JSON:
{
  "priority": "critical" | "high" | "medium" | "low",
  "actions": [
    { "action": "<what to do>", "owner": "<team: product | engineering | support | marketing>" }
  ],
  "shouldFollowUp": <boolean>,
  "followUpMessage": "<suggested response to the customer, if applicable>"
}`,
  model: openai('gpt-4o'),
  modelConfig: { temperature: 0.3 },
});

Step 3: Workflow

The workflow chains analysis steps and conditionally creates tickets:
workflows/analyze-feedback.ts
import { createWorkflow } from '@runflow-ai/sdk';
import { z } from 'zod';
import { sentimentAgent } from '../agents/sentiment';
import { actionAgent } from '../agents/action-recommender';

export const analyzeFeedbackWorkflow = createWorkflow({
  id: 'analyze-feedback',
  inputSchema: z.object({
    feedback: z.string(),
    customerEmail: z.string(),
    customerName: z.string(),
    source: z.string(),
  }),
  outputSchema: z.any(),
})
  // Step 1: Analyze sentiment
  .agent('analyze', sentimentAgent, {
    promptTemplate: `Analyze this customer feedback:

"{{input.feedback}}"

Customer: {{input.customerName}} ({{input.customerEmail}})
Source: {{input.source}}`,
  })

  // Step 2: Recommend actions
  .agent('recommend', actionAgent, {
    promptTemplate: `Original feedback: "{{input.feedback}}"
Customer: {{input.customerName}}

Sentiment analysis:
{{analyze.text}}

What actions should we take?`,
  })

  // Step 3: Create ticket if negative
  .condition(
    'check-negative',
    (ctx) => {
      try {
        const analysis = JSON.parse(ctx.stepResults.get('analyze').text);
        return analysis.sentiment === 'negative';
      } catch {
        return false;
      }
    },
    // Negative path: create ticket
    [
      {
        id: 'create-ticket',
        type: 'connector',
        config: {
          connector: 'hubspot',
          resource: 'tickets',
          action: 'create',
          parameters: {
            subject: 'Negative Feedback — {{input.customerName}}',
            content: `Feedback: {{input.feedback}}

Analysis: {{analyze.text}}

Recommended actions: {{recommend.text}}`,
            priority: 'high',
            category: 'feedback',
          },
        },
      },
    ],
    // Positive/neutral path: log only
    [
      {
        id: 'log-feedback',
        type: 'function',
        config: {
          execute: async (input, ctx) => {
            return { logged: true, sentiment: 'positive_or_neutral' };
          },
        },
      },
    ]
  )
  .build();

Step 4: Main Entry Point

main.ts
import { identify, track } from '@runflow-ai/sdk/observability';
import { analyzeFeedbackWorkflow } from './workflows/analyze-feedback';

export async function main(input: any) {
  // Validate
  if (!input?.feedback) {
    return { error: 'feedback is required' };
  }

  identify(input.customerEmail || input.customerId || 'anonymous');

  try {
    const result = await analyzeFeedbackWorkflow.execute({
      feedback: input.feedback,
      customerEmail: input.customerEmail || '',
      customerName: input.customerName || 'Unknown',
      source: input.source || 'api',
    });

    // Parse results for metrics
    let sentiment = 'unknown';
    let category = 'unknown';
    try {
      const analysis = JSON.parse(result.stepResults?.analyze?.text || '{}');
      sentiment = analysis.sentiment;
      category = analysis.category;
    } catch {}

    // Track feedback metrics
    track('feedback_analyzed', {
      sentiment,
      category,
      source: input.source || 'api',
      ticketCreated: sentiment === 'negative',
    });

    return {
      message: `Feedback analyzed: ${sentiment} sentiment, category: ${category}`,
      analysis: result.stepResults?.analyze?.text,
      actions: result.stepResults?.recommend?.text,
      ticketCreated: sentiment === 'negative',
    };
  } catch (error) {
    console.error('[feedback-analysis] Error:', error);
    return { error: 'Failed to analyze feedback' };
  }
}

Triggering the Pipeline

This workflow is typically triggered by external events, not user conversations:
# From a webhook (NPS survey, support form, review platform)
curl -X POST https://your-agent.runflow.ai/api \
  -H "Content-Type: application/json" \
  -d '{
    "feedback": "The product crashes every time I try to export. Very frustrated.",
    "customerEmail": "jane@example.com",
    "customerName": "Jane Doe",
    "source": "nps_survey"
  }'

How It Works

Feedback arrives (webhook, form, API)

┌────────────────────┐
│  Sentiment Agent    │  gpt-4o-mini → sentiment, category, themes
└──────┬─────────────┘

┌────────────────────┐
│  Action Agent       │  gpt-4o → priority, actions, follow-up
└──────┬─────────────┘

   Negative?
  ╱         ╲
 Yes         No
  ↓           ↓
Create     Log for
HubSpot    analytics
ticket

Track metrics

Key Patterns

Pipeline vs Conversation

This is a workflow (pipeline), not an agent (conversation). Data flows in, gets processed, and comes out — no back-and-forth with a user. Workflows are ideal for batch processing and event-driven automations.

Cheap Classification, Quality Recommendations

The sentiment agent uses gpt-4o-mini (fast, cheap). The action recommender uses gpt-4o (better reasoning). Match model cost to task complexity.

Structured JSON for Reliable Branching

Both agents return structured JSON so the workflow can reliably branch on results:
// Agent returns: { "sentiment": "negative", ... }
// Workflow condition parses and branches:
.condition('check', (ctx) => {
  const analysis = JSON.parse(ctx.stepResults.get('analyze').text);
  return analysis.sentiment === 'negative';
})

Next Steps

Workflows

Learn more about workflows

Connectors

HubSpot, Slack integrations

Multi-Agent System

Supervisor pattern

Observability

Track business metrics