Guide · AWS Comprehend · Events Detection
AWS Comprehend Events Detection in MCP Servers — EventTypeList, Entity Roles, Async Jobs
AWS Comprehend's Events detection API goes beyond named entity recognition to extract structured events — occurrences with a type, participants, time, and associated entities from unstructured text. Where DetectEntities tells you "AWS and John Smith are mentioned," Events detection tells you "ACQUISITION: AWS (SUBJECT) acquired John Smith's company (OBJECT) on April 12, 2026 (DATE_TIME)." This structured event representation is dramatically more useful for downstream reasoning: an MCP tool that extracts events can return structured data directly to an AI orchestrator rather than returning raw text and hoping the model infers the relationships. Comprehend supports 11 event types across financial, legal, and employment domains: ACQUISITION, BANKRUPTCY, BOARD_CHANGE, BUSINESS_EXPANSION, BUSINESS_REDUCTION, CORPORATE_ACQUISITION, DISSOLVED, EMPLOYMENT, IPO, RIGHTS_ISSUE, and SECONDARY_OFFERING. Events detection is async-only (no synchronous API) — you submit an S3 job and retrieve results from the output location. This guide covers the event type catalog, how to configure EventTypeList, the entity role taxonomy, the output JSON structure, and how to integrate events extraction into an MCP tool chain for financial document analysis.
TL;DR
Events detection is async-only: call StartEventsDetectionJobCommand with TargetEventTypes (your EventTypeList), wait for JobStatus: COMPLETED, then read the output JSON Lines from S3. Each output line contains an Events[] array where each event has a Type, a Triggers[] list (the text phrases that triggered the event), and an Arguments[] list (entity participants with roles: SUBJECT, OBJECT, DATE_TIME, ARTIFACT, INVESTOR, RECIPIENT, etc.). No custom model training required — events detection is built-in like PII detection. Supported only for English.
Supported event types and their entity roles
Each event type defines which entity roles are relevant. Understanding the role vocabulary is essential for correctly parsing the output:
| Event Type | Common Roles | Example |
|---|---|---|
| ACQUISITION | SUBJECT (acquirer), OBJECT (acquired), DATE_TIME, ARTIFACT (deal value) | "Anthropic acquired MCP Labs for $500M in March 2026" |
| BANKRUPTCY | SUBJECT (filing entity), DATE_TIME, ARTIFACT (filing type) | "MCP Corp filed for Chapter 11 bankruptcy on November 1" |
| EMPLOYMENT | SUBJECT (person), OBJECT (org), ARTIFACT (title), DATE_TIME | "Jane Doe joined AliveMCP as VP of Engineering last month" |
| IPO | SUBJECT (company), DATE_TIME, ARTIFACT (exchange, price) | "MCPCloud priced its IPO at $18/share on the NYSE" |
| BOARD_CHANGE | SUBJECT (person), OBJECT (company), ARTIFACT (board role) | "Three directors resigned from AliveMCP's board" |
| BUSINESS_EXPANSION | SUBJECT (company), ARTIFACT (location/product), DATE_TIME | "AliveMCP expanded MCP monitoring to 15 new regions" |
| BUSINESS_REDUCTION | SUBJECT (company), ARTIFACT (scope), DATE_TIME | "MCPStack reduced headcount by 20% this quarter" |
| DISSOLVED | SUBJECT (entity), DATE_TIME | "MCP Registry Ltd was dissolved in September 2026" |
| RIGHTS_ISSUE | SUBJECT (company), ARTIFACT (ratio, price), DATE_TIME | "AliveMCP offered a 1-for-4 rights issue at $8 per share" |
| SECONDARY_OFFERING | SUBJECT (company), ARTIFACT (shares, price), DATE_TIME | "MCPCloud completed a secondary offering of 5M shares" |
| CORPORATE_ACQUISITION | SUBJECT (acquirer), OBJECT (target), DATE_TIME, ARTIFACT | "MCPStack was acquired by CloudOps in a $1.2B deal" |
The distinction between ACQUISITION and CORPORATE_ACQUISITION is subtle: Comprehend uses ACQUISITION for the acquiring entity's perspective and CORPORATE_ACQUISITION when the target's perspective or the transaction itself is the subject in the source text. In practice, your extraction logic should handle both and merge duplicates by trigger text.
Starting an events detection job
import {
ComprehendClient,
StartEventsDetectionJobCommand,
DescribeEventsDetectionJobCommand,
LanguageCode,
} from '@aws-sdk/client-comprehend';
const client = new ComprehendClient({ region: 'us-east-1' });
const { JobId } = await client.send(new StartEventsDetectionJobCommand({
JobName: `events-detect-${Date.now()}`,
DataAccessRoleArn: 'arn:aws:iam::123456789012:role/ComprehendDataAccessRole',
LanguageCode: LanguageCode.EN,
TargetEventTypes: [
'ACQUISITION',
'CORPORATE_ACQUISITION',
'EMPLOYMENT',
'BOARD_CHANGE',
'BANKRUPTCY',
'BUSINESS_EXPANSION',
'BUSINESS_REDUCTION',
],
InputDataConfig: {
S3Uri: 's3://my-bucket/news-articles/',
InputFormat: 'ONE_DOC_PER_LINE', // or ONE_DOC_PER_FILE
},
OutputDataConfig: {
S3Uri: 's3://my-bucket/events-output/',
},
}));
// Poll until COMPLETED
async function waitForEventsJob(jobId: string): Promise<string> {
while (true) {
const { EventsDetectionJobProperties: props } = await client.send(
new DescribeEventsDetectionJobCommand({ JobId: jobId })
);
const status = props?.JobStatus;
if (status === 'COMPLETED') return props?.OutputDataConfig?.S3Uri ?? '';
if (status === 'FAILED') throw new Error(`Job failed: ${props?.Message ?? 'unknown'}`);
await new Promise(r => setTimeout(r, 30_000));
}
}
Specify only the event types relevant to your use case in TargetEventTypes. Processing is billed per character regardless of which types you specify, so there is no cost advantage to narrowing the list — but narrowing reduces output noise and makes post-processing simpler. Comprehend supports up to all 11 types simultaneously.
Output JSON structure
The output is a JSON Lines file (one JSON object per input document, matching input line order for ONE_DOC_PER_LINE). Each line has this structure:
// Example output line for: "AWS acquired Anthropic for $4B in a deal announced on April 1, 2026"
{
"Entities": [
{ "Id": 0, "Text": "AWS", "Type": "ORGANIZATION", "BeginOffset": 0, "EndOffset": 3 },
{ "Id": 1, "Text": "Anthropic", "Type": "ORGANIZATION", "BeginOffset": 13, "EndOffset": 22 },
{ "Id": 2, "Text": "$4B", "Type": "QUANTITY", "BeginOffset": 27, "EndOffset": 30 },
{ "Id": 3, "Text": "April 1, 2026", "Type": "DATE", "BeginOffset": 53, "EndOffset": 66 }
],
"Events": [
{
"Type": "ACQUISITION",
"Triggers": [
{
"Id": 4,
"Text": "acquired",
"Type": "ACQUISITION",
"BeginOffset": 4,
"EndOffset": 12,
"GroupScore": 1.0
}
],
"Arguments": [
{ "EntityId": 0, "Role": "SUBJECT", "Score": 0.98 },
{ "EntityId": 1, "Role": "OBJECT", "Score": 0.96 },
{ "EntityId": 2, "Role": "ARTIFACT", "Score": 0.87 },
{ "EntityId": 3, "Role": "DATE_TIME", "Score": 0.95 }
]
}
]
}
Key structural points:
- Entities[] — the full named entity list for the document (same as
DetectEntitiesoutput). Entity IDs are referenced byArguments[].EntityId. - Events[].Triggers[] — the text spans that triggered the event classification (the verb or noun that indicates an event occurred). A document may have multiple triggers for the same event type if the event is mentioned multiple times.
- Events[].Arguments[] — the entity participants with their roles.
EntityIdmaps into the top-levelEntities[]array byId. - GroupScore on triggers — confidence that this trigger is a genuine event indicator (distinct from role assignment scores in Arguments).
Parsing events output in TypeScript
interface ComprehendEntity {
Id: number;
Text: string;
Type: string;
BeginOffset: number;
EndOffset: number;
}
interface EventArgument {
EntityId: number;
Role: string;
Score: number;
}
interface ComprehendEvent {
Type: string;
Triggers: Array<{ Id: number; Text: string; GroupScore: number }>;
Arguments: EventArgument[];
}
interface DocumentEventsOutput {
Entities: ComprehendEntity[];
Events: ComprehendEvent[];
}
interface StructuredEvent {
type: string;
trigger: string;
confidence: number;
subject?: string;
object?: string;
date?: string;
artifact?: string;
investor?: string;
recipient?: string;
}
function parseEvents(output: DocumentEventsOutput): StructuredEvent[] {
const entityMap = new Map(output.Entities.map(e => [e.Id, e]));
return output.Events.map(event => {
const structured: StructuredEvent = {
type: event.Type,
trigger: event.Triggers[0]?.Text ?? '',
confidence: event.Triggers[0]?.GroupScore ?? 0,
};
for (const arg of event.Arguments) {
const entity = entityMap.get(arg.EntityId);
if (!entity || arg.Score < 0.75) continue;
switch (arg.Role) {
case 'SUBJECT': structured.subject = entity.Text; break;
case 'OBJECT': structured.object = entity.Text; break;
case 'DATE_TIME': structured.date = entity.Text; break;
case 'ARTIFACT': structured.artifact = entity.Text; break;
case 'INVESTOR': structured.investor = entity.Text; break;
case 'RECIPIENT': structured.recipient = entity.Text; break;
}
}
return structured;
}).filter(e => e.confidence >= 0.70);
}
The role assignment score (Arguments[].Score) is separate from the trigger confidence (Triggers[].GroupScore). A high trigger confidence (0.95) with low role assignment score (0.50) for SUBJECT means Comprehend is confident an acquisition event happened but uncertain which entity was the acquirer. Apply independent thresholds: trigger confidence ≥ 0.70 to include the event at all, role score ≥ 0.75 to assign a role field, and skip role assignment below that (leave the field undefined rather than assigning a wrong entity).
MCP tool pattern for financial document events
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import * as readline from 'node:readline';
import { Readable } from 'node:stream';
const server = new McpServer({ name: 'financial-events', version: '1.0.0' });
const s3 = new S3Client({ region: 'us-east-1' });
// Tool 1: submit documents for events extraction
server.tool('extract_financial_events_start', {
document_texts: z.array(z.string()).max(100),
event_types: z.array(z.enum([
'ACQUISITION', 'CORPORATE_ACQUISITION', 'EMPLOYMENT', 'IPO',
'BOARD_CHANGE', 'BANKRUPTCY', 'BUSINESS_EXPANSION', 'BUSINESS_REDUCTION',
'DISSOLVED', 'RIGHTS_ISSUE', 'SECONDARY_OFFERING',
])).default(['ACQUISITION', 'CORPORATE_ACQUISITION', 'EMPLOYMENT']),
}, async ({ document_texts, event_types }) => {
// Write documents to S3 as newline-delimited file
const inputKey = `events-input/${Date.now()}.jsonl`;
const content = document_texts.join('\n');
await s3.send(new PutObjectCommand({
Bucket: process.env.COMPREHEND_BUCKET!,
Key: inputKey,
Body: content,
ContentType: 'text/plain',
}));
const { JobId } = await client.send(new StartEventsDetectionJobCommand({
JobName: `events-${Date.now()}`,
DataAccessRoleArn: process.env.COMPREHEND_ROLE_ARN!,
LanguageCode: LanguageCode.EN,
TargetEventTypes: event_types,
InputDataConfig: {
S3Uri: `s3://${process.env.COMPREHEND_BUCKET!}/${inputKey}`,
InputFormat: 'ONE_DOC_PER_LINE',
},
OutputDataConfig: {
S3Uri: `s3://${process.env.COMPREHEND_BUCKET!}/events-output/`,
},
}));
return { content: [{ type: 'text', text: JSON.stringify({ jobId: JobId }) }] };
});
// Tool 2: check job status and retrieve results when ready
server.tool('extract_financial_events_results', {
job_id: z.string(),
}, async ({ job_id }) => {
const { EventsDetectionJobProperties: props } = await client.send(
new DescribeEventsDetectionJobCommand({ JobId: job_id })
);
if (props?.JobStatus !== 'COMPLETED') {
return { content: [{ type: 'text', text: JSON.stringify({
status: props?.JobStatus,
message: 'Job not complete yet — check again in 60 seconds',
})}] };
}
// Read output from S3 and parse events
const outputUri = props.OutputDataConfig?.S3Uri ?? '';
// ... parse S3 output, apply parseEvents(), return structured events
return { content: [{ type: 'text', text: JSON.stringify({ outputUri, status: 'COMPLETED' }) }] };
});
The two-tool submit/retrieve pattern prevents tool handler timeout: submitting a Comprehend events job returns immediately with a JobId, while the actual processing takes minutes. The AI orchestrator calls the submit tool, stores the JobId, waits 60–120 seconds, then calls the results tool to check completion. This is the same pattern used for all async AWS processing jobs (Textract, Comprehend batch, SageMaker Transform).
Multi-document event deduplication
When processing a corpus of news articles about the same company, the same acquisition event will appear in dozens of documents with slightly different trigger phrases and entity mentions. Deduplication is necessary before presenting results to downstream consumers:
function deduplicateEvents(allEvents: StructuredEvent[]): StructuredEvent[] {
// Group by (type, subject, object) — consider these the same event
const seen = new Map<string, StructuredEvent>();
for (const event of allEvents) {
const key = [
event.type,
event.subject?.toLowerCase() ?? '',
event.object?.toLowerCase() ?? '',
].join('|');
const existing = seen.get(key);
if (!existing || event.confidence > existing.confidence) {
// Keep the highest-confidence mention as the canonical event
seen.set(key, event);
}
}
return Array.from(seen.values())
.sort((a, b) => b.confidence - a.confidence);
}
For temporal deduplication (same event mentioned across documents from different dates), include a normalized date in the deduplication key when the date field is available. Events with the same type/subject/object but dates more than 30 days apart are likely separate events.