Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
Regulatory Change Monitoring
[YourCompany.com] · Legal Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for the FullSpec team building the Regulatory Change Monitoring automation. It covers the current-state step map, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything needed to begin implementation without further discovery is contained here. Where a decision has not yet been locked down, a constraint or confirmation requirement is called out explicitly so the FullSpec team can resolve it before touching the relevant agent.
01Process snapshot
02Agent specifications
Runs on a daily schedule to fetch new content from all configured regulatory feeds and websites via Feedly, deduplicates items already seen in the Airtable intake table, and passes only net-new content downstream to the Relevance and Summary Agent. This agent eliminates the need for any manual website visiting and is the entry point for the entire automation pipeline. It must be stable before any downstream agent build begins.
// Input
feedly.stream_id : string // Feedly collection ID for regulatory sources
feedly.newer_than : unix_ms // Timestamp of last successful poll (stored in Airtable config row)
feedly.count : integer // Max items per poll, default 50
// Deduplication lookup (Airtable)
airtable.table : 'Regulatory_Intake'
airtable.filter : {feedly_article_id} = '{article.id}'
// Output (passed to Relevance and Summary Agent)
article.id : string // Feedly article fingerprint
article.title : string
article.source_name : string // Feed label as configured in Feedly collection
article.source_url : string
article.published_at : ISO8601
article.full_text : string // Extracted body text, max 4000 chars
article.canonical_url : string
article.is_duplicate : boolean // true = skip; false = pass downstream- Feedly plan must be Pro+ or Business to enable the Collections API with full-text extraction. The Teams plan (from $18/month) covers this; confirm the account tier before build.
- The Feedly collection named 'Regulatory Sources' must be pre-populated with all 42 confirmed sources before agent testing begins. The source list was signed off on Apr 15.
- Deduplication key is the Feedly article ID stored in the Airtable Regulatory_Intake table field feedly_article_id. On first run, seed the table with the last 30 days of article IDs to prevent a flood of historic items being treated as new.
- If a source is not available via RSS and requires a custom web watcher, that watcher must be configured and tested separately before the source is added to the Feedly collection or an equivalent polling step. Flag any login-gated or JavaScript-rendered sources to FullSpec before build; they require a separate scraping approach.
- The agent must write a last_polled_at timestamp to the Airtable config row after each successful run so the next poll uses the correct newer_than value.
- If the Feedly API returns a 429 rate-limit response, the agent must back off for 60 seconds and retry once before writing an error log entry and halting that run gracefully.
- Items with is_duplicate = true must be silently archived in the automation log; no downstream call should be made for them.
Receives each net-new article from the Source Monitor Agent and applies an AI classification step to score relevance against the business's configured jurisdiction profile and topic tag list. Items that score below the configured threshold are discarded silently. Items above the threshold receive a two-to-three sentence plain-language summary, an extracted or inferred effective date, and a suggested internal owner derived from the topic-to-owner routing table stored in Airtable. Borderline items (within a configurable band around the threshold) are held and routed to the Compliance Coordinator for a 5-minute manual review before any further action is taken.
// Input (from Source Monitor Agent)
article.id : string
article.title : string
article.source_name : string
article.published_at : ISO8601
article.full_text : string
article.canonical_url : string
// Classification prompt context (retrieved from Notion page 'Relevance Config')
config.jurisdictions : string[] // e.g. ['AU', 'NZ', 'UK']
config.topic_tags : string[] // e.g. ['AML', 'Privacy', 'Financial Services', 'OH&S']
config.keywords : string[] // Boosted signal terms
config.threshold_pass : float // default 0.70
config.threshold_review: float // default 0.50 (below this = discard)
// Routing table lookup (Airtable table: 'Routing_Rules')
routing.topic_tag : string // matched tag
routing.owner_name : string
routing.owner_slack_id : string
routing.owner_email : string
// Output (written to Airtable table: 'Regulatory_Register')
register.article_id : string
register.title : string
register.source_name : string
register.source_url : string
register.published_at : ISO8601
register.effective_date: ISO8601 | null // extracted by AI; null if not found
register.topic_tag : string
register.relevance_score: float
register.summary : string // 2-3 sentence plain-language summary
register.suggested_owner: string
register.owner_slack_id: string
register.status : enum('pending_review', 'approved', 'discarded')
register.flagged_borderline: boolean
// On borderline (threshold_review <= score < threshold_pass)
// Pause pipeline; send borderline alert to Compliance Coordinator Slack DM
// Await manual status update in Airtable (approved | discarded) before Alert Agent fires- The AI classification model must use a structured prompt that includes jurisdiction list, topic tags, and keywords retrieved fresh from the Notion 'Relevance Config' page at each invocation, not cached at build time. This allows the business to update rules without a code change.
- Relevance score must be returned as a float between 0.0 and 1.0 with the reasoning in a separate field for logging. The reasoning field is written to Airtable but not surfaced to end users.
- Default threshold_pass is 0.70 and threshold_review is 0.50. These must be stored as editable Airtable config fields, not hard-coded, so the business can tune them post-launch.
- Effective date extraction should use a secondary AI prompt pass on the article text. If no explicit effective date is found, the field is set to null and the summary must note 'Effective date not specified'.
- The topic-to-owner routing table in Airtable (table: Routing_Rules) must have at least one catch-all row where topic_tag = 'Unclassified' with a default owner set to the Compliance Coordinator. Build must not proceed to Alert Agent testing until this row is present.
- If the classification API call fails (timeout or error), the item must be written to the register with status = 'pending_review' and flagged_borderline = true so it surfaces for human review rather than being silently dropped.
- Confirm with the FullSpec team whether the Notion integration uses an internal integration token or OAuth; the workspace admin must authorise the Notion connection before this agent can read the Relevance Config page.
Handles all outbound communication for the pipeline. It fires immediately after a new record with status = 'approved' is written to the Airtable Regulatory_Register, posting a structured Slack alert tagged to the responsible owner in the correct topic channel. On a Monday morning schedule it assembles the previous week's approved items into a digest email and sends it via Gmail to the configured management recipients. It also monitors the register for action items that remain unacknowledged beyond a configurable number of days and sends overdue follow-up Slack messages to the responsible owner.
// Input (real-time alert path)
register.record_id : string
register.title : string
register.summary : string
register.effective_date: ISO8601 | null
register.topic_tag : string
register.relevance_score: float
register.source_url : string
register.owner_slack_id: string
register.suggested_owner: string
// Input (weekly digest path)
// Query Airtable Regulatory_Register WHERE created_at >= last Monday 00:00
// AND status = 'approved'
// ORDER BY relevance_score DESC
// Input (overdue follow-up path)
// Query Airtable Regulatory_Register WHERE acknowledged_at IS NULL
// AND status = 'approved'
// AND created_at < NOW() - overdue_days
config.overdue_days : integer // default 7; stored in Airtable config table
// Output (real-time alert)
slack.channel : string // resolved from topic_tag via Routing_Rules
slack.mention : string // owner_slack_id formatted as <@U...>
slack.blocks : object[] // Slack Block Kit message with title, summary, effective date, source link, and acknowledge button
// Output (weekly digest)
gmail.to : string[] // management recipient list from Airtable config
gmail.subject : string // 'Weekly Regulatory Digest – {ISO week label}'
gmail.html_body : string // Templated HTML listing each item: title, summary, owner, effective date, register link
// Output (overdue follow-up)
slack.dm_to : string // owner_slack_id
slack.message : string // Plain text overdue reminder with item title and register deep link
// On acknowledge (Slack interactive button callback)
airtable.update : {acknowledged_at: ISO8601, acknowledged_by: slack_user_id}- Slack Block Kit must be used for real-time alerts, not plain text, so the acknowledge button can be rendered inline. The Slack app must have the chat:write and chat:write.public OAuth scopes and the interactivity webhook URL must be registered before end-to-end testing.
- The acknowledge button callback must write acknowledged_at and acknowledged_by back to the Airtable record. This is the mechanism that prevents overdue follow-ups for items that have been seen.
- Gmail sending must use a dedicated service account or an OAuth2 refresh token associated with the compliance team's Gmail workspace, not a personal account. Confirm the Gmail workspace tier supports third-party OAuth app connections before build.
- The weekly digest email template must be built as a reusable HTML template with inline CSS only (no external stylesheets) to ensure compatibility across mail clients. Sections: header with week label, ranked item list, footer with register link and unsubscribe note.
- The digest recipient list must be stored in an Airtable config table field (digest_recipients), not hard-coded, so it can be updated without a deployment change.
- The overdue_days config value defaults to 7 and must also be stored in the Airtable config table. Build must include a guard to ensure the overdue check never sends more than one follow-up per item per 24-hour window to avoid spamming owners.
- If a Slack alert fails to post (channel not found, user not in workspace), the error must be logged to the error log table and a fallback email must be sent to the owner's address stored in Routing_Rules.
03End-to-end data flow
// ============================================================
// REGULATORY CHANGE MONITORING: END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// Scheduled daily at 07:00 local (configurable)
// OR Feedly webhook fires on new item (Pro+ plan required)
TRIGGER -> Source Monitor Agent
inputs:
feedly.stream_id = 'user/{feedly_user_id}/category/Regulatory_Sources'
feedly.newer_than = airtable.config['last_polled_at'] // unix_ms
feedly.count = 50
// ============================================================
// AGENT HANDOFF 1: Source Monitor Agent -> Deduplication
// ============================================================
for each article in feedly.response.items:
article.id -> airtable.lookup('Regulatory_Intake', feedly_article_id)
IF match found:
article.is_duplicate = true // skip; no downstream call
log('duplicate_skipped', article.id)
ELSE:
airtable.insert('Regulatory_Intake', {
feedly_article_id : article.id,
title : article.title,
source_name : article.source_name,
source_url : article.canonical_url,
published_at : article.published_at,
full_text : article.full_text[0:4000],
intake_status : 'queued'
})
article.is_duplicate = false
-> pass to Relevance and Summary Agent
airtable.config['last_polled_at'] = NOW() // update poll timestamp
// ============================================================
// AGENT HANDOFF 2: Source Monitor Agent -> Relevance and Summary Agent
// ============================================================
Relevance and Summary Agent receives:
article.id, article.title, article.source_name,
article.published_at, article.full_text, article.canonical_url
// Step A: Fetch classification config from Notion
notion.page('Relevance Config') ->
config.jurisdictions : string[]
config.topic_tags : string[]
config.keywords : string[]
config.threshold_pass : float // from Airtable config table
config.threshold_review : float // from Airtable config table
// Step B: AI classification call
prompt_input:
system : 'You are a regulatory compliance classifier. Score relevance 0.0-1.0.'
context : config.jurisdictions, config.topic_tags, config.keywords
content : article.title + article.full_text
prompt_output:
relevance_score : float
matched_topic_tag : string
reasoning : string // logged, not surfaced
// Step C: Route on score
IF relevance_score >= threshold_pass:
classification_status = 'approved'
ELSE IF relevance_score >= threshold_review:
classification_status = 'borderline' // hold for human review
ELSE:
classification_status = 'discarded'
log('item_discarded', article.id, relevance_score)
STOP
// Step D: Summary generation (approved + borderline only)
prompt_input:
system : 'Write a 2-3 sentence plain-language summary of this regulatory change.'
content : article.full_text
prompt_output:
summary : string
effective_date : ISO8601 | null
// Step E: Routing table lookup
airtable.lookup('Routing_Rules', {topic_tag: matched_topic_tag}) ->
routing.owner_name : string
routing.owner_slack_id : string
routing.owner_email : string
FALLBACK: topic_tag = 'Unclassified' -> Compliance Coordinator
// Step F: Write to Regulatory_Register
airtable.insert('Regulatory_Register', {
article_id : article.id,
title : article.title,
source_name : article.source_name,
source_url : article.canonical_url,
published_at : article.published_at,
effective_date : effective_date,
topic_tag : matched_topic_tag,
relevance_score : relevance_score,
reasoning : reasoning,
summary : summary,
suggested_owner : routing.owner_name,
owner_slack_id : routing.owner_slack_id,
owner_email : routing.owner_email,
status : classification_status,
flagged_borderline : (classification_status == 'borderline'),
acknowledged_at : null,
acknowledged_by : null,
created_at : NOW()
})
IF classification_status == 'borderline':
slack.dm(owner_slack_id = COMPLIANCE_COORDINATOR_SLACK_ID,
message = 'Borderline item requires review: {title} | {airtable_record_link}')
PAUSE pipeline for this item; await manual status update in Airtable
ELSE IF classification_status == 'approved':
-> pass record_id to Alert and Digest Agent (immediate path)
// ============================================================
// AGENT HANDOFF 3: Relevance and Summary Agent -> Alert and Digest Agent
// ============================================================
Alert and Digest Agent: immediate path
trigger : Airtable record status updated to 'approved'
inputs : register.record_id -> fetch full record
// Post Slack Block Kit alert
slack.post({
channel : resolve_channel(matched_topic_tag), // from Routing_Rules
blocks: [
header : 'New Regulatory Change: {title}',
section : summary,
fields : [effective_date, source_name, topic_tag, relevance_score],
actions : [button('Acknowledge', value=record_id)]
]
})
Alert and Digest Agent: weekly digest path
trigger : Monday 08:00 scheduled run
query : Airtable Regulatory_Register WHERE
created_at >= last_monday AND status = 'approved'
ORDER BY relevance_score DESC
gmail.send({
to : airtable.config['digest_recipients'],
subject : 'Weekly Regulatory Digest – {ISO_week_label}',
html_body : render_digest_template(items)
})
Alert and Digest Agent: overdue follow-up path
trigger : Daily 09:00 scheduled run
query : Airtable Regulatory_Register WHERE
acknowledged_at IS NULL AND status = 'approved'
AND created_at < NOW() - config.overdue_days
for each record:
IF last_overdue_sent_at < NOW() - 24h OR last_overdue_sent_at IS NULL:
slack.dm(owner_slack_id, 'Overdue action: {title} | {airtable_record_link}')
airtable.update(record_id, {last_overdue_sent_at: NOW()})
// ============================================================
// SLACK INTERACTIVE CALLBACK (acknowledge button)
// ============================================================
slack.callback.payload.actions[0].value = record_id
slack.callback.user.id = slack_user_id
-> airtable.update('Regulatory_Register', record_id, {
acknowledged_at : NOW(),
acknowledged_by : slack_user_id
})
// ============================================================
// ERROR LOGGING (all agents)
// ============================================================
ON any agent error:
airtable.insert('Error_Log', {
agent_name : string,
error_type : string,
error_message : string,
article_id : string | null,
timestamp : ISO8601
})
IF severity == 'critical':
slack.dm(FULLSPEC_ALERT_CHANNEL, 'Automation error: {error_message}')04Recommended build stack
More documents for this process
Every document generated for Regulatory Change Monitoring.