Skip to main content

Overview

GTM Feedback uses Workflow DevKit (use workflow) for durable execution of background tasks.
Workflows are long-running processes that can be paused, resumed, and retried automatically. They’re perfect for AI operations, external API calls, and multi-step processes.

What are Workflows?

Workflows provide durable execution for:
  • AI-powered processing - Semantic matching, request creation, insights generation
  • External integrations - Slack notifications, webhook calls
  • Multi-step orchestration - Fan-out/fan-in patterns, sequential steps
  • Resilient operations - Automatic retries, error recovery

Feedback Processing

Match feedback to feature requests using AI agents

Request Creation

Generate new feature requests from feedback

Insights Generation

Analyze product areas and generate reports

Slack Integration

Send notifications and approval requests

Workflow Structure

Workflows are located in apps/www/src/workflows/ organized by domain:
workflows/
├── area-insights/              # Product area insights generation
│   ├── index.ts
│   └── steps/
│       └── generate-area-insights.ts
├── entry/
│   └── new-entry/             # New feedback entry processing
│       ├── index.ts
│       └── steps/
├── feedback/                  # Feedback-related workflows
├── request/                   # Feature request workflows
│   ├── new-request/
│   └── match-request/
├── semantic-search/           # AI semantic matching
├── sync-embeddings/           # Vector embedding sync
└── shared/                    # Shared workflow utilities
    ├── get-all-product-areas.ts
    └── track-event.ts

Creating a Workflow

Basic Workflow Pattern

Every workflow function must include the "use workflow" directive:
export async function myWorkflow(args: Args) {
  "use workflow";

  // workflow logic here
  const result = await someStep(args);
  
  return { status: "success", result };
}

Real-World Example: Area Insights

From workflows/area-insights/index.ts:
import { createLogger } from "@feedback/config/logger";
import { SERVER_ENV } from "@/lib/env.server";

import { getAllProductAreas } from "../shared/get-all-product-areas";
import { generateAreaInsightsStep } from "./steps/generate-area-insights";

const logger = createLogger("area-insights-cron");

/**
 * workflow to regenerate area insights for all product areas.
 * triggered by the daily cron job at 8am ET.
 */
export async function generateAllAreaInsightsWorkflow() {
  "use workflow";

  logger.info("Starting daily insights regeneration...");

  const areas = await getAllProductAreas();
  const areasWithSlugs = areas.filter(
    (area): area is typeof area & { slug: string } =>
      typeof area.slug === "string" && area.slug.length > 0,
  );

  logger.info("Found areas to process", {
    withSlugs: areasWithSlugs.length,
    total: areas.length,
  });

  if (areasWithSlugs.length === 0) {
    logger.info("No areas to process, exiting.");
    return {
      status: "success",
      processedCount: 0,
      failedSlugs: [],
    };
  }

  const userId = SERVER_ENV.SYSTEM_USER_ID;
  const results = await Promise.all(
    areasWithSlugs.map(async (area) => {
      try {
        const result = await generateAreaInsightsStep({
          slug: area.slug,
          userId,
        });
        logger.info(`Processed area: ${area.slug}`, {
          success: result.success,
          durationMs: result.durationMs,
        });
        return result;
      } catch (error) {
        logger.error(`Unexpected error for ${area.slug}`, {
          error: error instanceof Error ? error.message : "Unknown error",
        });
        return {
          success: false,
          slug: area.slug,
          hasOpenRequests: false,
          durationMs: 0,
          error: error instanceof Error ? error.message : "Unknown error",
        };
      }
    }),
  );

  const successful = results.filter((r) => r.success);
  const failed = results.filter((r) => !r.success);

  logger.info("Insights regeneration complete", {
    succeeded: successful.length,
    failed: failed.length,
  });

  if (failed.length > 0) {
    logger.warn("Some areas failed", {
      failedSlugs: failed.map((r) => r.slug),
    });
  }

  return {
    status: "success",
    processedCount: successful.length,
    failedSlugs: failed.map((r) => r.slug),
  };
}

Workflow Patterns

Pattern 1: Parallel Execution (Fan-out)

Execute multiple steps in parallel:
export async function parallelWorkflow(args: Args) {
  "use workflow";

  // fan out to multiple steps in parallel
  const [result1, result2, result3] = await Promise.all([
    step1(args),
    step2(args),
    step3(args)
  ]);

  return { result1, result2, result3 };
}

Pattern 2: Sequential Execution

Execute steps one after another:
export async function sequentialWorkflow(args: Args) {
  "use workflow";

  // step 1: fetch data
  const data = await fetchData(args.id);

  // step 2: process with AI
  const processed = await processWithAI(data);

  // step 3: save results
  await saveResults(processed);

  return { status: "success" };
}

Pattern 3: Conditional Execution

Execute steps based on conditions:
export async function conditionalWorkflow(args: Args) {
  "use workflow";

  const data = await fetchData(args.id);

  if (data.confidence >= 0.9) {
    // high confidence: auto-approve
    await autoApprove(data);
  } else if (data.confidence >= 0.8) {
    // medium confidence: request approval
    await requestHumanApproval(data);
  } else {
    // low confidence: create new
    await createNew(data);
  }

  return { status: "success" };
}

Pattern 4: Error Recovery

Handle errors gracefully:
export async function resilientWorkflow(args: Args) {
  "use workflow";

  try {
    const result = await riskyOperation(args);
    return { status: "success", result };
  } catch (error) {
    logger.error("Operation failed, attempting recovery", { error });
    
    // attempt recovery
    const fallback = await fallbackOperation(args);
    
    return { 
      status: "recovered", 
      result: fallback,
      error: error instanceof Error ? error.message : "Unknown"
    };
  }
}

Workflow Steps

Organize complex logic into reusable steps:
import { createLogger } from "@feedback/config/logger";

const logger = createLogger("my-step");

type StepArgs = {
  id: string;
  data: unknown;
};

export async function myStep(args: StepArgs) {
  logger.info("Executing step", { args });

  // step logic here
  const result = await processData(args.data);

  logger.info("Step complete", { result });

  return result;
}

Testing Workflows Locally

1

Start the development server

pnpm dev
Workflows run automatically when triggered.
2

Trigger a workflow

Workflows can be triggered from:
  • API routes - POST /api/workflows/my-workflow
  • Server actions - Call workflow from an action
  • Cron jobs - Scheduled execution
  • Event handlers - Slack events, webhooks
3

Monitor execution

Check the console logs for workflow execution:
# filter logs by workflow name
pnpm dev | grep "my-workflow"
4

Debug with logging

Add logging to your workflow:
import { createLogger } from "@feedback/config/logger";

const logger = createLogger("my-workflow");

export async function myWorkflow(args: Args) {
  "use workflow";
  
  logger.info("Starting", { args });
  logger.debug("Processing step 1");
  logger.info("Complete");
}

Workflow Execution & Monitoring

Triggering Workflows

app/api/workflows/my-workflow/route.ts
import { myWorkflow } from "@/workflows/my-workflow";

export async function POST(request: Request) {
  const body = await request.json();
  
  // trigger workflow
  const result = await myWorkflow(body);
  
  return Response.json(result);
}

Monitoring Best Practices

Structured Logging

Use consistent log levels and structured data

Error Tracking

Log errors with context for debugging

Duration Tracking

Measure and log execution time

Result Reporting

Return meaningful status and results

AI Integration in Workflows

Many workflows use AI agents from packages/ai/src/agents/:
import { semanticSearchAgent } from "@feedback/ai/agents/semantic-search";

export async function matchFeedbackWorkflow(args: Args) {
  "use workflow";

  // use AI agent for semantic matching
  const matches = await semanticSearchAgent({
    query: args.feedbackText,
    limit: 5
  });

  // process matches
  const bestMatch = matches[0];
  
  if (bestMatch.confidence >= 0.9) {
    await autoLinkFeedback(args.feedbackId, bestMatch.requestId);
  }

  return { status: "success", matches };
}

Common Workflow Operations

Invalidate cache after mutations:
import { revalidatePath } from "next/cache";

export async function myWorkflow(args: Args) {
  "use workflow";
  
  await performMutation(args);
  
  // revalidate affected paths
  revalidatePath(`/requests/${args.slug}`);
  revalidatePath("/dashboard");
  
  return { status: "success" };
}
Make HTTP requests to external services:
export async function callExternalAPI(args: Args) {
  "use workflow";
  
  const response = await fetch("https://api.example.com/endpoint", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(args)
  });
  
  if (!response.ok) {
    throw new Error(`API call failed: ${response.statusText}`);
  }
  
  const data = await response.json();
  return { status: "success", data };
}
Integrate with Slack:
import { sendSlackMessage } from "@/lib/slack/client";

export async function notifySlackWorkflow(args: Args) {
  "use workflow";
  
  await sendSlackMessage({
    channel: process.env.SLACK_CHANNEL_ID,
    text: `New feedback from ${args.accountName}`,
    blocks: [...]
  });
  
  return { status: "success" };
}
Handle large datasets efficiently:
export async function batchProcessWorkflow(args: Args) {
  "use workflow";
  
  const batchSize = 10;
  const items = await fetchItems();
  
  // process in batches
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    
    await Promise.all(
      batch.map(item => processItem(item))
    );
    
    logger.info(`Processed batch ${i / batchSize + 1}`);
  }
  
  return { status: "success", processed: items.length };
}

Best Practices

Always use 'use workflow'

Required directive at the top of workflow functions

Structured logging

Use consistent logger with workflow name

Error handling

Catch errors and return meaningful status

Idempotency

Design workflows to be safely retried

Small steps

Break complex workflows into focused steps

Type safety

Define clear TypeScript types for args and returns

Next Steps

Contributing

Read contribution guidelines

Workflow DevKit

Learn more about Workflow DevKit

Build docs developers (and LLMs) love