Docs/Recipes/Pipeline bot

Build a pipeline bot.

A working bot that posts pipeline movement to Slack and writes a weekly forecast note for the head of sales. About 40 lines of TOML, 30 lines of Python. Copy and adapt.

What it does

  1. Watches every Opportunity in CRM for stage transitions.
  2. Posts a one-line update to #sales-pipeline in Slack when an opp moves up.
  3. Every Friday at 4pm, drafts a forecast doc summarizing the week's movement and sends it to the head of sales for review.

Step 1: workflow for stage changes

[workflow]
name = "pipeline-bot-stage"

[trigger]
event = "opportunity.stage_changed"

[scope]
read  = ["crm:opportunities"]
write = ["slack.post_message"]

[[steps]]
name = "notify"
tool = "slack.post_message"
input = {
  channel = "#sales-pipeline",
  text    = "$trigger.opportunity.name, $trigger.from_stage → $trigger.to_stage  (${ '$' }$trigger.opportunity.arr_usd ARR)",
}

Apply with wrxstack workflow apply ./pipeline-bot-stage.toml. Within seconds, every stage transition posts to Slack.

Step 2: Friday forecast

[workflow]
name = "pipeline-bot-forecast"

[trigger]
event = "schedule.weekly"
cron  = "0 16 * * 5"            # 4pm Fridays

[scope]
read  = ["crm:opportunities", "crm:accounts"]
write = ["documents.create", "slack.post_message"]

[[steps]]
name      = "draft"
assistant = "forecast"      # custom assistant (next step)
input     = { period = "this_week" }

[[steps]]
name = "share"
tool = "slack.post_message"
input = {
  channel = "#head-of-sales",
  text    = "📊 Weekly forecast ready: $steps.draft.doc_url",
}

Step 3: the forecast assistant

from wrxstack import tool, assistant

@tool(name="forecast.weekly", scope=["crm:opportunities.read", "documents.write"])
def weekly_forecast(period: str = "this_week") -> dict:
    opps = client.crm.opportunities.list(
        moved_since="this_week",
        stages=["Closing", "Negotiation", "Closed-Won"],
    )

    body = render_template(opps)
    doc = client.documents.create(
        title=f"Forecast, week of {today():%b %d}",
        body=body,
        format="markdown",
    )
    return {"doc_url": doc.url, "opp_count": len(opps)}

forecast_assistant = assistant(
    name="forecast",
    description="Drafts the weekly pipeline forecast.",
    tools=[weekly_forecast],
    model="anthropic/claude-3-5-sonnet",
)

Audit the first runs

Apply both workflows. Wait for a real stage change. Open the audit log → filter by workflow name. You should see:

  • One row per stage transition with the slack message id.
  • One row Friday at 4pm with the draft doc id.
  • Zero rows under "scope_denied." If you see any, your scope block is too tight.

Variations

  • Restrict to opps above a dollar threshold.
  • Mention the AE on every Slack post by reading opp.owner.
  • Generate a second forecast for the CFO with a different lens.