In B2B enterprise sales, timing matters. When a target account discloses a financial shift, such as rising spend in a category you sell into, a restructuring, or a capital reallocation, the account executive (AE) who notices first usually has the advantage. Public companies disclose these shifts constantly in 10-Ks, 10-Qs, 8-K earnings releases and earnings call materials. Most of that information is never turned into a timely, account-specific alert for the person who owns the relationship.

This article shows how to close that gap. The pipeline watches for new filings, has an OpenAI model analyze them with the Code Interpreter tool, applies deterministic qualification rules, and posts a structured alert to the right AE in Slack using Block Kit.

A note on terminology. “Advanced Data Analysis” is the name of the feature inside ChatGPT. In the OpenAI API the same capability is the Code Interpreter tool, which lets the model write and run Python in a sandbox. This article builds on the API.

A note on currency. OpenAI scheduled the Assistants API to sunset on August 26, 2026, and recommends the Responses API for new work. Many older tutorials, including earlier versions of this one, use Assistants, threads and runs. The code below uses the Responses API.

The Problem: Data Density vs. Sales Speed

Consider what happens to a financial disclosure inside a typical sales organization:

1. Ingestion: The disclosure arrives as a long PDF, an HTML filing, or a spreadsheet.

2. Analysis lag: Someone has to find the relevant line items, normalize them and calculate period-over-period changes.

3. Distribution friction: The result lands in an email thread, a wiki page or a slide deck.

4. Action decay: By the time the AE reads it, the news may be days old and competitors may already have reached out.

Dashboards in tools like Tableau, Power BI or Salesforce reports don’t fix this, because they depend on the AE going to look. Alerts work better when they are pushed into the channel where reps already work, in a short, scannable format with a clear next step.

Architecture Overview

The pipeline has five stages:

```

[ Filing source: SEC EDGAR / uploaded reports ]

│

▼

[ OpenAI Responses API + Code Interpreter ]

│ (Python sandbox: extraction, normalization, deltas)

▼

[ Orchestrator: Python service / n8n / serverless function ]

│ (validation, thresholds, CRM lookup, AE routing)

▼

[ Slack Web API: chat.postMessage with Block Kit ]

│

▼

[ AE notification + interactive actions ]

```

1. Detect and fetch new filings

For U.S. public companies, the SEC’s EDGAR system is the natural source. EDGAR offers free JSON APIs on `data.sec.gov`, including a submissions endpoint (a filer’s filing history) and a company facts endpoint (the XBRL financial data the company has reported). No API key is needed, but the SEC asks automated clients to declare a descriptive `User-Agent` with contact details and to stay within a current maximum of 10 requests per second.

Two practical points:

  • Prefer structured data when it exists. XBRL facts from the company facts API give you tagged numbers directly, which is more reliable than extracting them from a PDF layout. Use Code Interpreter for the parts that structured data doesn’t cover, such as narrative sections, tables in earnings decks, non-public spreadsheets or messy exports.
  • Watch the right forms. 10-Ks and 10-Qs carry the full financials. Earnings releases are typically furnished on Form 8-K, which can surface results sooner.

2. Analyze with Code Interpreter

Code Interpreter gives the model a sandboxed container where it can write and run Python. According to OpenAI’s documentation:

  • The container is a fully sandboxed virtual machine. Memory tiers are 1 GB (the default), 4 GB, 16 GB and 64 GB, with higher tiers billed at the built-in tools rate.
  • Files you pass as model input are automatically made available in the container, and supported formats include PDF, CSV, XLSX, DOCX, JSON and plain text.
  • Containers expire after 20 minutes of inactivity, so you should treat them as ephemeral and download any files you need while they are active.
  • The tool is rate limited to 100 requests per minute per organization.

Typical jobs for the model here:

  • Pull tables out of multi-column or multi-page layouts.
  • Normalize periods and units, then compute year-over-year (YoY) and quarter-over-quarter (QoQ) changes.
  • Isolate line items relevant to your product category, such as technology spend, cloud costs or capital expenditure.

3. Qualify the signal deterministically

Not every change is a sales trigger. Have the model return structured JSON containing the raw numbers it extracted, and apply your qualification rules in ordinary code afterward. That keeps the decision auditable and easy to tune. Examples of rules:

  • The change in a target category exceeds your threshold (for instance, 15% YoY).
  • The absolute amount is large enough to matter for your deal size.
  • The account falls in a segment you actively sell into.

4. Route to the right AE

Match the company to your CRM (Salesforce, HubSpot or similar) using a stable identifier such as ticker, CIK or a domain, rather than a company name string. Look up the account owner, the pipeline stage and the account URL. If no owner is assigned, fall back to a team channel or a RevOps queue.

5. Deliver in Slack

Slack’s Block Kit lets you build messages from structured blocks such as headers, sections, dividers and buttons. Slack’s Block Kit Builder is a handy tool for prototyping layouts.

Implementation

The examples below are a starting point rather than production code.

Step 1: Upload the report and run the analysis

```

import json

import os

from openai import OpenAI

client = OpenAI() # reads OPENAI_API_KEY from the environment

MODEL = os.environ["OPENAI_MODEL"] # choose a current model from OpenAI's model docs

INSTRUCTIONS = """You are a financial analyst supporting enterprise sales.

Use the python tool to extract figures from the attached report and compute

changes between periods. Report only numbers that appear in the document, and

include the page or section where each figure was found. If a figure is not

present, return null instead of estimating it."""

PROMPT = """Identify line items related to cloud infrastructure, software

licensing, IT services, or digital transformation spend. For each, return the

current value, the prior-period value, the currency and units, and the

percentage change you computed.

Respond with JSON only, matching this shape:

{

"company_name": str,

"period": str,

"items": [{

"metric": str,

"current": number | null,

"prior": number | null,

"units": str,

"pct_change": number | null,

"source_location": str

}],

"narrative_signals": [str],

"recommended_sales_angle": str

}"""

def analyze_report(path: str) -> dict:

with open(path, "rb") as f:

uploaded = client.files.create(file=f, purpose="user_data")

response = client.responses.create(

model=MODEL,

tools=[{

"type": "code_interpreter",

"container": {"type": "auto", "memory_limit": "4g"},

}],

tool_choice="required",

instructions=INSTRUCTIONS,

input=[{

"role": "user",

"content": [

{"type": "input_file", "file_id": uploaded.id},

{"type": "input_text", "text": PROMPT},

],

}],

)

return json.loads(response.output_text) # add retry/validation in production

```

Setting `tool_choice="required"` forces the model to use the tool, and the docs note that the most explicit way to invoke Code Interpreter is to ask for “the python tool” in your prompt.

Step 2: Apply qualification rules in code

```

THRESHOLD_PCT = 15.0

def qualifying_items(insight: dict) -> list[dict]:

hits = []

for item in insight.get("items", []):

pct = item.get("pct_change")

cur, prior = item.get("current"), item.get("prior")

if pct is None or cur is None or prior in (None, 0):

continue

Recompute the delta yourself instead of trusting the model's arithmetic.

recomputed = (cur - prior) / abs(prior) * 100

if abs(recomputed - pct) > 0.5:

continue # inconsistent output: route to human review instead

if recomputed >= THRESHOLD_PCT:

hits.append({**item, "pct_change": round(recomputed, 1)})

return hits

```

Recomputing the percentage change, rather than trusting the model’s number, gives you a cheap consistency check.

Step 3: Post to Slack

Two Slack constraints affect the design:

  • Incoming webhooks are bound to one channel. Slack-app webhooks can no longer override the destination channel or author identity at runtime. To route different alerts to different AEs, use the Web API method `chat.postMessage` with a bot token (the `chat:write` scope), passing a channel ID or a user ID for a direct message.
  • Action buttons need an interactivity endpoint. A button that triggers server-side logic requires interactivity to be enabled in your Slack app, with a Request URL that receives and acknowledges the click. Buttons that only open a link (using a `url` field) don’t need that.

```

import os

from slack_sdk import WebClient

slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])

def send_slack_alert(insight: dict, hits: list[dict], ae_slack_id: str, report_url: str):

top = max(hits, key=lambda h: h["pct_change"])

summary = (

f"*{insight['company_name']}* ({insight['period']}): "

f"`{top['metric']}` up *{top['pct_change']}%*"

)

blocks = [

{"type": "header",

"text": {"type": "plain_text", "text": "Financial signal detected"}},

{"type": "section", "text": {"type": "mrkdwn", "text": summary}},

{"type": "section", "text": {"type": "mrkdwn",

"text": f"*Why it may matter:*\n{insight['recommended_sales_angle']}"}},

{"type": "context", "elements": [{"type": "mrkdwn",

"text": f"Source: {top['source_location']}. Verify before outreach."}]},

{"type": "actions", "elements": [

{"type": "button", "text": {"type": "plain_text", "text": "View source report"},

"url": report_url},

Requires interactivity to be configured on your Slack app:

{"type": "button", "style": "primary", "action_id": "claim_account",

"text": {"type": "plain_text", "text": "Claim account"},

"value": insight["company_name"]},

]},

]

Passing a user ID as `channel` sends the message to that user in a DM with the app.

return slack.chat_postMessage(

channel=ae_slack_id,

text=f"New signal for {insight['company_name']}", # fallback for notifications

blocks=blocks,

)

```

The top-level `text` field serves as the notification fallback, so it should be meaningful on its own.

Designing Alerts AEs Will Read

  • Lead with the trigger. One line: who, what changed, by how much, and over which period.
  • Show the evidence. Include the figure’s source location so the AE can verify it in seconds.
  • Keep the angle short. One or two sentences on why this could matter for your product.
  • Offer one or two actions. Open the source, claim the account, or open the CRM record are enough.
  • Don’t invent urgency. A severity label should come from your rules, not from the model’s tone.

Measuring Success

Track outcomes rather than volume:

1. Time-to-outreach: the time between a filing’s public availability and the AE’s first relevant touchpoint. Establish your own baseline first. Published benchmarks for this specific workflow are scarce, so measure before and after rather than assuming a target.

2. Response and meeting rates: compare outreach that references a specific disclosure against your standard outreach.

3. Alert quality: the share of alerts AEs mark as useful, claim, or act on. A falling rate is an early sign of alert fatigue.

4. Pipeline velocity: whether signal-sourced opportunities move through stages faster than a comparable control group.

Common Pitfalls

  • Model errors still happen. Running code lets the model compute rather than guess, but it can still extract the wrong row, mix up periods or misread units. Require source locations, recompute deltas in your own code, cross-check against XBRL facts where available, and send low-confidence results to human review.
  • Alert fatigue. Set conservative thresholds, deduplicate by account and period, and cap alerts per AE per day.
  • Ownership data. Routing is only as good as your CRM hygiene. Define fallback rules for unassigned accounts and for accounts with several owners.
  • Fair-access compliance. If you pull from EDGAR, declare your `User-Agent`, respect the rate limit, and consider the SEC’s bulk files for large backfills.
  • Ephemeral containers. Code Interpreter containers expire after 20 minutes of inactivity. Persist outputs you need on your own systems.
  • Sensitive data. Public filings are low risk. If you analyze internal or customer spreadsheets, review OpenAI’s data controls and your own compliance requirements first.
  • API drift. Model names, tools and SDK parameters change quickly. Pin SDK versions, read the current docs before deploying, and avoid building on deprecated surfaces such as the Assistants API.

Conclusion

The idea is simple: when a target account discloses something meaningful, the right AE should hear about it quickly, with the evidence attached and a sensible next step. Code Interpreter handles the messy extraction and arithmetic, ordinary code handles qualification and routing, and Slack handles delivery. Keep humans in the loop for verification, tune thresholds against real AE feedback, and treat the pipeline as a decision-support tool rather than an autonomous sales engine.