---
title: "HackerOne Reports to a Slack Summary"
canonical: https://trickest.com/docs/using-the-app/connectors/reports-to-slack
description: "Collect a report sample, build a reviewable summary, and optionally post it once to Slack."
---

# HackerOne Reports to a Slack Summary

Collect reports from HackerOne, prepare a text summary, and review it before adding a Slack send node. Keep collection, normalization and delivery as separate workflow steps so you can inspect each result.

## Before you start

Complete [HackerOne: Collect program reports](/docs/using-the-app/connectors/hackerone) and [Slack: Read recent messages](/docs/using-the-app/connectors/slack). You need access to the programs and an approved destination channel. Report content can be private; decide which fields the Slack audience should receive.

## Collect a bounded sample

Build the two-node HackerOne workflow with `programs` and `reports`. Start with two program handles, at most five reports per program, and a report-node call limit of `2`. Keep the report result as **JSON** and choose **When any call fails**.

Run these read steps and inspect the program handles, report IDs, report states and any errors. A report limit or a workflow name containing “new” does not select only new reports.

## Normalize the records

Add a Python node named `report-rows` and connect the **folder** output from `reports`. This script expects the per-line JSON call records produced by **For each line** with **Save as > JSON**. Each record carries the program in `item`, success in `ok`, and the tool response in `result`. It reads the mounted file at `in/reports/output.txt`, handles the versioned MCP response wrapper, and writes one normalized record per report.

```python
import json
from pathlib import Path


def payload(value):
    if isinstance(value, dict) and value.get("version") == 1:
        if set(value) == {"version", "result"}:
            return value["result"]
        if value.get("structuredContent") is not None:
            return value["structuredContent"]
        parts = [json.loads(part["text"]) for part in value.get("content", [])
                 if part.get("type") == "text"]
        if len(parts) == 1:
            return parts[0]
    return value


source = Path("in/reports/output.txt")
rows = []
for line in source.read_text().splitlines():
    if not line.strip():
        continue
    call = json.loads(line)
    if call.get("ok") is not True or not isinstance(call.get("item"), str):
        raise ValueError("A report call failed or lacks its program handle")
    result = payload(call["result"])
    if not isinstance(result, dict) or not isinstance(result.get("items"), list):
        raise ValueError("Expected a report result with an items list")
    for report in result["items"]:
        if not isinstance(report, dict) or report.get("id") is None:
            raise ValueError("A report lacks its ID")
        rows.append({"program": call["item"], "report_id": str(report["id"]),
                     "state": report.get("state", "unknown")})
Path("out").mkdir(exist_ok=True)
Path("out/reports.jsonl").write_text(
    "".join(json.dumps(row) + "\n" for row in rows)
)
```

Run `report-rows` and inspect `out/reports.jsonl`. Each row keeps the program handle, stable report ID and state. A failed call or unexpected shape stops the script instead of silently reducing the report count. Empty `items` lists produce an empty report file. This mapping matches the Trickest HackerOne adapter; recheck the contract if you use another custom server.

The normalized file omits report titles and descriptions. Add those fields only if the destination audience may see them. The next script uses program/report identities to remove duplicate records.

For a recurring “new reports” digest, first add a persisted sent-report ledger and compare stable IDs with it. Update that ledger only after confirmed delivery. Until then, describe this as a report sample; rerunning it can repeat reports.

## Build a text file for review

Add a Python node named `summary`, connect the `report-rows` output folder, and use this standard-library script. It reads the normalized contract above and writes one summary file. Its input path assumes the upstream node is named `report-rows`.

```python
import json
from collections import Counter
from pathlib import Path

files = sorted(Path("in/report-rows").rglob("reports.jsonl"))
if not files:
    raise ValueError("No normalized report file found")
rows = [json.loads(line) for file in files
        for line in file.read_text().splitlines() if line.strip()]
unique = {(row["program"], str(row["report_id"])): row for row in rows}
counts = Counter(row["program"] for row in unique.values())
lines = [f"HackerOne report sample: {len(unique)} reports"]
lines.extend(f"{program}: {count}" for program, count in sorted(counts.items()))
lines.append("Bounded sample; check collection limits and failures before use.")
Path("out").mkdir(exist_ok=True)
Path("out/output.txt").write_text("\n".join(lines) + "\n")
```

Run `summary` and open `out/output.txt` in **Files**. Check the count against the normalized records. This summary sends counts only; it does not include report descriptions or confidential attachments. Stop here until the text and destination are ready.

<Frame editorial maxWidth="600px" darkSrc="https://res.cloudinary.com/db14crach/image/upload/v1792713960/www/docs/release-1-2-1-2026-10-23/product/reports-summary-pattern-dark-b7012f8742b41a2d.png" caption="Part of the saved pattern: collect reports, normalize them and build summary text. Slack delivery is the optional final step outside this crop.">
  <img src="https://res.cloudinary.com/db14crach/image/upload/v1792713962/www/docs/release-1-2-1-2026-10-23/product/reports-summary-pattern-light-d4a597dca513f00b.png" alt="HackerOne programs and reports pass through normalization and summary nodes." />
</Frame>

## Add one Slack post after review

1. Use Slack channel search to confirm the intended channel ID, workspace and audience. Do not rely on a channel name alone.
2. Add `slack_send_message` and name it `post-summary`.
3. Connect the `summary` output under **Data** and keep **Run** set to **Once**.
4. Set **Channel id** (`channel_id`) to the confirmed destination.
5. In **Message** (`message`), use **Insert into Message > Connected nodes** to choose `summary` as **Whole file**. Keep **Save as** set to **JSON**.
6. Run the post node only when you intend to send the reviewed message. Inspect its result for an error or returned message reference, then verify the message in the destination channel.

<Warning>
The current tool schema calls the required body field **Message** (`message`). An older saved node may show **Text** (`text`); review and update that binding against the discovered schema before execution.

Use **Once** for the sender. **For each line** would turn a multi-line summary into multiple calls. Retrying or rerunning a send can create duplicate messages; check the channel after an uncertain outcome before retrying.
</Warning>

## Check failures before scheduling

Do not schedule this workflow until you have tested incomplete collection, empty results and delivery failure. Keep report counts, collection limits and skipped programs visible. If you add a sent-report ledger, preserve its state when Slack delivery fails.

The screenshot shows the historical saved example. No new Slack message or HackerOne call was made while preparing this local tutorial. See [Using connectors in workflows](/docs/using-the-app/connectors/using-connectors-in-workflows) for whole-file inputs and partial failures.

---
_Markdown source of https://trickest.com/docs/using-the-app/connectors/reports-to-slack._
