Skip to main content
Glama

outreach.plan

Read-onlyIdempotent

Turn high-intent leads into a structured Week-1 outreach plan with per-channel daily send targets and per-category action register. Use after leads.find to get actionable steps.

Instructions

Build a Week-1 outreach plan from a completed run's HIGH-intent leads, with per-channel send cadence and per-category action register. Behavior: client-side synthesis. Fetches the run via runs.get (no extra credit), buckets HIGH leads (lead_score >= 0.7) by source and matched_signals category, then applies fixed cadence heuristics (Reddit / X tolerate 3-4 sends/day; YouTube / TikTok / Instagram only 2 because each comment is more visible). Idempotent and free. Usage: call this immediately after leads.find completes if the user wants a concrete action plan rather than a raw lead dump. Skip it if HIGH lead count is under 5 (the heuristic falls apart on tiny pools, refine the idea and re-run instead). Do NOT call this on a still-running run, results will be incomplete. Returns: a multi-line text plan with the HIGH/MED/total breakdown, per-channel daily send target + follow-up window, per-category action register (ACTIVE_SEARCH, PAIN_OR_FRUSTRATION, SWITCHING, COMPARISON, FEATURE_GAP, COMPETITOR, TUTORIAL, DISCUSSION), and an end-of-week deprioritisation rule.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
run_idYesThe run_id returned by leads.find

Implementation Reference

  • src/index.ts:729-731 (registration)
    Registration of the 'outreach.plan' tool on the MCP server via server.tool(), with description, schema (run_id), and metadata hints.
    server.tool(
      "outreach.plan",
      "Build a Week-1 outreach plan from a completed run's HIGH-intent leads, with per-channel send cadence and per-category action register. Behavior: client-side synthesis. Fetches the run via runs.get (no extra credit), buckets HIGH leads (lead_score >= 0.7) by source and matched_signals category, then applies fixed cadence heuristics (Reddit / X tolerate 3-4 sends/day; YouTube / TikTok / Instagram only 2 because each comment is more visible). Idempotent and free. Usage: call this immediately after leads.find completes if the user wants a concrete action plan rather than a raw lead dump. Skip it if HIGH lead count is under 5 (the heuristic falls apart on tiny pools, refine the idea and re-run instead). Do NOT call this on a still-running run, results will be incomplete. Returns: a multi-line text plan with the HIGH/MED/total breakdown, per-channel daily send target + follow-up window, per-category action register (ACTIVE_SEARCH, PAIN_OR_FRUSTRATION, SWITCHING, COMPARISON, FEATURE_GAP, COMPETITOR, TUTORIAL, DISCUSSION), and an end-of-week deprioritisation rule.",
  • Handler function for outreach.plan: validates API key, fetches run results, filters HIGH (>=0.7) and MED leads, groups by source and category, applies per-channel cadence heuristics, builds and returns a text plan with send targets and action register.
    async ({ run_id }) => {
      const err = requireKey();
      if (err) return err;
    
      const result = await call<RunResult>("GET", `get-run?id=${run_id}`);
      const posts = result.results;
    
      if (posts.length === 0) {
        return {
          content: [
            {
              type: "text" as const,
              text: `Run ${run_id} returned no leads. Re-run with a sharper idea before planning a funnel.`,
            },
          ],
        };
      }
    
      const high = posts.filter((p) => p.lead_score >= 0.7);
      const med = posts.filter((p) => p.lead_score >= 0.4 && p.lead_score < 0.7);
    
      // Group HIGH by source and category
      const bySource = new Map<string, Post[]>();
      const byCategory = new Map<string, number>();
      for (const p of high) {
        const arr = bySource.get(p.source) ?? [];
        arr.push(p);
        bySource.set(p.source, arr);
        const cat = p.matched_signals.find((s) => s.startsWith("category:"))?.slice(9) ?? "OTHER";
        byCategory.set(cat, (byCategory.get(cat) ?? 0) + 1);
      }
    
      // Per-channel cadence heuristic. Reddit + X tolerate higher daily volume than
      // YouTube/TikTok/Instagram, where each comment is more visible to the creator.
      const VOLUME = {
        reddit: { perDay: 3, followUpDays: 5 },
        twitter: { perDay: 4, followUpDays: 3 },
        x: { perDay: 4, followUpDays: 3 },
        youtube: { perDay: 2, followUpDays: 7 },
        tiktok: { perDay: 2, followUpDays: 7 },
        instagram: { perDay: 2, followUpDays: 7 },
      } as Record<string, { perDay: number; followUpDays: number }>;
    
      const ACTION_BY_CATEGORY: Record<string, string> = {
        ACTIVE_SEARCH: "Direct answer. They're asking, you have it. Reply within 24h.",
        PAIN_OR_FRUSTRATION: "Empathy first, link second. Acknowledge the pain in your own words before mentioning the product.",
        SWITCHING: "Position as the next step, not 'a' next step. Reference what they said they're leaving.",
        COMPARISON: "Insert as the third option in their list. One concrete tradeoff vs each they named.",
        FEATURE_GAP: "Lead with the missing feature. Skip the rest of the pitch.",
        COMPETITOR: "INTEL ONLY. Do not DM. Use to understand positioning.",
        TUTORIAL: "Skip for outreach. Use for keyword research and content ideas.",
        DISCUSSION: "Comment publicly, not via DM. Lower-effort, lower-conversion.",
      };
    
      const channelLines: string[] = [];
      let totalWeeklySends = 0;
      for (const [source, leads] of bySource.entries()) {
        const v = VOLUME[source] ?? { perDay: 2, followUpDays: 5 };
        const weekly = Math.min(leads.length, v.perDay * 7);
        totalWeeklySends += weekly;
        channelLines.push(
          `  ${source}: ${leads.length} HIGH leads. Send ${v.perDay}/day (${weekly} this week). Follow up after ${v.followUpDays} days if no reply.`,
        );
      }
    
      const categoryLines: string[] = [];
      for (const [cat, n] of [...byCategory.entries()].sort((a, b) => b[1] - a[1])) {
        categoryLines.push(`  ${cat} (${n}): ${ACTION_BY_CATEGORY[cat] ?? "Use the per-lead opener."}`);
      }
    
      const text = [
        `Acquisition funnel for run ${run_id}`,
        ``,
        `Pool: ${high.length} HIGH, ${med.length} MED, ${posts.length} total.`,
        ``,
        `Per-channel cadence (Week 1):`,
        ...channelLines,
        ``,
        `Total Week 1 send target: ${totalWeeklySends} messages.`,
        ``,
        `Action per category:`,
        ...categoryLines,
        ``,
        `Workflow:`,
        `  1. Triage HIGH leads. Drop the COMPETITOR / TUTORIAL ones.`,
        `  2. For each remaining lead, call outreach.draft with the right source + outreach_action.`,
        `  3. Hand-edit each draft for one specific detail from the post.`,
        `  4. Send. Log it. Move on.`,
        `  5. After 7 days, follow up only on the channels listed above. One follow-up max.`,
        ``,
        `If conversion is below 5% after 50 sends on a channel, deprioritise that channel and re-allocate to whichever is converting.`,
      ].join("\n");
    
      return {
        content: [{ type: "text" as const, text }],
      };
    },
  • Input schema for outreach.plan: expects a single 'run_id' string parameter validated with Zod.
    {
      run_id: z.string().describe("The run_id returned by leads.find"),
    },
  • Generic HTTP client (call<T>) used by the handler to fetch run results from the API.
    async function call<T>(
      method: "GET" | "POST" | "DELETE",
      endpoint: string,
      body?: unknown
    ): Promise<T> {
      const cfg = await getConfig();
      const res = await fetch(`${cfg.api_base}/${endpoint}`, {
        method,
        headers: {
          "Content-Type": "application/json",
          "x-api-key": GORILLA_API_KEY,
          apikey: cfg.gateway_key,
        },
        ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
      });
    
      if (!res.ok) {
        const text = await res.text().catch(() => "");
        throw new Error(`${method} /${endpoint} failed (${res.status}): ${text}`);
      }
    
      return res.json() as Promise<T>;
    }
  • requireKey() helper used by the handler to check GORILLA_API_KEY is set.
    function requireKey() {
      if (!GORILLA_API_KEY) {
        return {
          content: [
            {
              type: "text" as const,
              text: "GORILLA_API_KEY is not set. Sign up at https://usegorilla.app, then create a key at gorilla.opusforge.com.br > Menu > API Keys and set GORILLA_API_KEY in your environment.",
            },
          ],
        };
      }
      return null;
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate idempotent and read-only; description adds client-side synthesis, fetching via runs.get, bucketing leads at >=0.7, per-channel cadence heuristics (Reddit/X 3-4/day, others 2/day), and free. No contradictions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-structured with purpose first, then behavior, usage, returns. Somewhat lengthy but every sentence adds value. Could be slightly more concise but clear.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given single param with full schema and no output schema, description fully explains return format (multi-line text with breakdown, cadence, action register, deprioritisation rule) and prerequisites (run completed, HIGH leads >=5). Complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter (run_id) with 100% schema coverage, description merely restates the schema (run_id returned by leads.find). Adds no new meaning beyond schema, so baseline 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it builds a Week-1 outreach plan from HIGH-intent leads with per-channel cadence and per-category action register. Distinguishes from siblings like leads.find (raw lead dump) and outreach.draft (drafting messages).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says to call immediately after leads.find, skip if HIGH lead count is under 5 (heuristic fails), and not to call on still-running run. Provides clear when-to-use and when-not-to-use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/opusforge/gorilla-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server