DAG Artifacts System - User Guide¶
Overview¶
The DAG Artifacts system allows external data processing pipelines to: 1. Register a processing workflow (DAG) with the system 2. Fetch work items that need processing 3. Process the data using your AI models or algorithms 4. Submit results back to the system 5. Query artifacts using positive and negative filters 6. Automatically execute registered actions (write-back to database, create summaries, etc.)
This guide walks through three complete examples: Classification/Scoring, Summarization, and Topic-Level Processing.
Prerequisites¶
- API Key: You need a valid API key (X-API-Key header)
- Base URL:
http://localhost:8000/api/v1/artifacts(or your production URL) - External Processing System: Your AI/ML pipeline that will process the work items
Understanding Artifacts Table¶
Every work item creates an artifact that tracks its processing lifecycle:
┌─────────────────────────────────────────────────────────────┐
│ ARTIFACT RECORD │
├─────────────────────────────────────────────────────────────┤
│ artifact_id: UUID (unique identifier) │
│ dag_id: UUID (which DAG processes this) │
│ source_kind: 'record' | 'topic' | 'feed' | 'tag' | 'custom' │
│ source_table: 'crawl_outcomes' | 'feed_entries' etc. │
│ source_id: UUID (ID of source record) │
│ source_metadata: {} (additional context) │
│ state: 'pending' → 'sent_for_processing' → 'completed' │
│ input_data: {...} (data sent to DAG) │
│ output_data: {...} (results from DAG) │
│ created_at, sent_at, completed_at (timestamps) │
└─────────────────────────────────────────────────────────────┘
Key Concepts:
- source_kind: What type of logical grouping (record-level, topic-level, etc.)
- source_metadata: Extra context about the source (topic name, feed URL, etc.)
- Positive filters: Match specific values (?state=completed)
- Negative filters: Exclude values (?state_not=completed)
- List filters: Match/exclude multiple values (?state_in=pending,sent_for_processing)
Discovery: What's Available?¶
Before registering a DAG, explore what tables, columns, and actions are available.
Get Available Tables & Columns¶
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
http://localhost:8000/api/v1/artifacts/docs/available-tables | python3 -m json.tool
Response:
{
"tables": {
"crawl_outcomes": {
"columns": [
{"name": "id", "type": "uuid"},
{"name": "extracted_metadata", "type": "jsonb"},
{"name": "status", "type": "character varying"},
{"name": "ranking_needed", "type": "boolean"},
{"name": "relevance_score", "type": "integer"}
],
"commonly_used": ["id", "extracted_metadata", "status"]
}
}
}
Get Available Action Types¶
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
http://localhost:8000/api/v1/artifacts/docs/action-types | python3 -m json.tool
Response:
{
"action_types": {
"write_back": {
"name": "Write Back",
"description": "Updates the source record with values from DAG output",
"use_cases": ["Setting scores", "Updating flags", "Adding timestamps"]
},
"insert_into_table": {
"name": "Insert Into Table",
"description": "Creates new records in a different table with DAG output",
"use_cases": ["Storing summaries", "Creating entities"]
}
}
}
Example 1: Classification/Scoring DAG¶
Use Case¶
You have a machine learning model that classifies regulatory documents and assigns a relevance score (0-100). You want to:
- Fetch unscored documents from crawl_outcomes
- Process them with your ML model
- Write the score back to crawl_outcomes.relevance_score
- Mark them as processed (ranking_needed = false)
Step 1: Register the Classification DAG¶
Create a configuration file classification_dag.json:
{
"name": "Classification DAG",
"description": "Classifies feed entries and assigns relevance scores",
"source_table": "crawl_outcomes",
"source_columns": ["id", "extracted_metadata"],
"input_selector": {
"filter": {
"ranking_needed": true,
"status": "completed"
},
"order_by": ["created_at"],
"limit": 100
},
"output_schema": {
"type": "string",
"kind": "string",
"score": "integer"
},
"registered_actions": [
{
"action_type": "write_back",
"target_table": "crawl_outcomes",
"match_column": "id",
"mappings": [
{
"output_key": "score",
"target_column": "relevance_score"
}
],
"flags_to_set": [
{
"column": "ranking_needed",
"value": false
},
{
"column": "ranking_completed_at",
"value": "NOW()"
},
{
"column": "ranking_execution_id",
"value": "{{execution_id}}"
}
]
}
],
"is_active": true,
"config_metadata": {
"dag_type": "classification",
"version": "1.0"
}
}
Register the DAG:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
-d @classification_dag.json \
http://localhost:8000/api/v1/artifacts/dags | python3 -m json.tool
Response:
{
"id": "1d22ab5d-3058-4f4d-8fbf-5595be353aa2",
"dag_id": "153524a3-1e10-460c-b8a3-3ce431257058",
"name": "Classification DAG",
"is_active": true,
...
}
⚠️ IMPORTANT: Save the dag_id (UUID) - you'll need it for all subsequent API calls!
Step 2: Fetch Work Items (GET)¶
Fetch documents that need classification:
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
"http://localhost:8000/api/v1/artifacts/dags/153524a3-1e10-460c-b8a3-3ce431257058/work-items?limit=10" \
| python3 -m json.tool
Response:
{
"dag_id": "153524a3-1e10-460c-b8a3-3ce431257058",
"work_items": [
{
"artifact_id": "01f64840-a64c-4e32-ba81-d65092844d0f",
"source_id": "7b41098c-6270-4adb-bd68-0c00f2018833",
"input_data": {
"id": "7b41098c-6270-4adb-bd68-0c00f2018833",
"extracted_metadata": {
"url": "https://cma.gov.sa/...",
"title": "Capital Market Authority Report",
"s3_content_md_path": "s3://bucket/path/to/content.md",
"assets": [...]
}
},
"created_at": "2025-12-08T15:48:08.755803+00:00"
},
...
],
"count": 10,
"cache_hit": false
}
Key Fields:
- artifact_id: Unique ID for this processing unit (use when submitting results)
- source_id: ID of the crawl_outcome record
- input_data.extracted_metadata.s3_content_md_path: Where to read the content from
Step 3: Process Items with Your ML Model¶
This is your external processing code (Python example):
import boto3
import requests
import openai # or your ML library
API_BASE = "http://localhost:8000/api/v1/artifacts"
API_KEY = "jKA3nuqP1maUyeFTRX23ieYbezaynBR4"
DAG_ID = "153524a3-1e10-460c-b8a3-3ce431257058"
# Step 1: Fetch work items
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/work-items?limit=10",
headers={"X-API-Key": API_KEY}
)
work_items = response.json()["work_items"]
# Step 2: Process each item
results = []
s3 = boto3.client('s3')
for item in work_items:
artifact_id = item["artifact_id"]
s3_path = item["input_data"]["extracted_metadata"]["s3_content_md_path"]
# Read content from S3
bucket, key = parse_s3_path(s3_path)
content = s3.get_object(Bucket=bucket, Key=key)["Body"].read().decode()
# Classify with your ML model
classification = classify_document(content) # Your ML code
# Returns: {"type": "regulatory_announcement", "kind": "enforcement", "score": 85}
# Store result
results.append({
"artifact_id": artifact_id,
"output_data": classification,
"state": "completed",
"execution_id": "batch-run-2025-12-08-001"
})
print(f"Processed {len(results)} items")
Step 4: Submit Results (POST)¶
Submit your classification results back to the system:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
-d '{
"results": [
{
"artifact_id": "01f64840-a64c-4e32-ba81-d65092844d0f",
"output_data": {
"type": "regulatory_announcement",
"kind": "enforcement_action",
"score": 85
},
"state": "completed",
"execution_id": "batch-run-2025-12-08-001"
},
{
"artifact_id": "3bb4bd8d-3fb1-40e7-b45d-29d8dc09939b",
"output_data": {
"type": "regulatory_announcement",
"kind": "guidance_document",
"score": 72
},
"state": "completed",
"execution_id": "batch-run-2025-12-08-001"
}
]
}' \
http://localhost:8000/api/v1/artifacts/dags/153524a3-1e10-460c-b8a3-3ce431257058/results \
| python3 -m json.tool
Response:
{
"processed": 2,
"failed": 0,
"action_results": [
{
"artifact_id": "01f64840-a64c-4e32-ba81-d65092844d0f",
"actions": {
"actions_executed": 1,
"results": [
{
"success": true,
"action_type": "write_back",
"target_table": "crawl_outcomes",
"updated_count": 1,
"update_fields": {
"relevance_score": "85",
"ranking_needed": "False",
"ranking_completed_at": "2025-12-08 15:52:30.566189+00:00",
"ranking_execution_id": "batch-run-2025-12-08-001"
}
}
]
}
}
]
}
What Happened Automatically:
1. ✅ Artifacts table updated with your scores
2. ✅ crawl_outcomes.relevance_score set to 85, 72
3. ✅ crawl_outcomes.ranking_needed set to false
4. ✅ crawl_outcomes.ranking_completed_at set to current timestamp
5. ✅ crawl_outcomes.ranking_execution_id set to your execution ID
Example 2: Summarization DAG¶
Use Case¶
You want to generate role-specific summaries of regulatory documents using GPT-4. You want to:
- Fetch documents that don't have summaries yet
- Generate summaries for different roles (Compliance Officer, CEO, Legal)
- Store summaries in dag_summaries table (not update the source)
Step 1: Register the Summarization DAG¶
Create summarization_dag.json:
{
"name": "Summarization DAG",
"description": "Generates role-specific summaries for feed entries",
"source_table": "crawl_outcomes",
"source_columns": ["id", "extracted_metadata", "source_id"],
"input_selector": {
"filter": {
"status": "completed",
"summary_completed_at__isnull": true
},
"order_by": ["created_at"],
"limit": 100
},
"output_schema": {
"summary": "string",
"key_points": "array[string]",
"org_id": "uuid",
"role": "string"
},
"registered_actions": [
{
"action_type": "insert_into_table",
"target_table": "dag_summaries",
"mappings": [
{
"output_key": "summary",
"target_column": "summary_text"
},
{
"output_key": "key_points",
"target_column": "key_points"
},
{
"output_key": "org_id",
"target_column": "organization_id"
},
{
"output_key": "role",
"target_column": "user_role"
},
{
"source_column": "id",
"target_column": "crawl_outcome_id"
}
]
}
],
"is_active": true,
"config_metadata": {
"dag_type": "summarization",
"creates_summaries": true,
"version": "1.0"
}
}
Register:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
-d @summarization_dag.json \
http://localhost:8000/api/v1/artifacts/dags | python3 -m json.tool
Save the returned dag_id: 6527c9b7-25ee-4728-a237-aa3e8e62db94
Step 2: Fetch Work Items (GET)¶
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
"http://localhost:8000/api/v1/artifacts/dags/6527c9b7-25ee-4728-a237-aa3e8e62db94/work-items?limit=5" \
| python3 -m json.tool
Response: (Same structure as classification example)
Step 3: Process Items - Generate Summaries¶
import boto3
import requests
from openai import OpenAI
API_BASE = "http://localhost:8000/api/v1/artifacts"
API_KEY = "jKA3nuqP1maUyeFTRX23ieYbezaynBR4"
DAG_ID = "6527c9b7-25ee-4728-a237-aa3e8e62db94"
client = OpenAI()
# Fetch work items
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/work-items?limit=5",
headers={"X-API-Key": API_KEY}
)
work_items = response.json()["work_items"]
# Process each item
results = []
s3 = boto3.client('s3')
for item in work_items:
artifact_id = item["artifact_id"]
s3_path = item["input_data"]["extracted_metadata"]["s3_content_md_path"]
# Read content
bucket, key = parse_s3_path(s3_path)
content = s3.get_object(Bucket=bucket, Key=key)["Body"].read().decode()
# Generate summary with GPT-4
prompt = f"""
Summarize the following regulatory document for a Compliance Officer.
Focus on:
- Key regulatory changes
- Impact on operations
- Action items
Document:
{content[:4000]}
"""
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
summary = completion.choices[0].message.content
# Extract key points (simplified)
key_points = [
"New compliance requirement effective Q1 2026",
"Enhanced reporting obligations for transactions > $1M",
"90-day implementation period"
]
# Store result
results.append({
"artifact_id": artifact_id,
"output_data": {
"summary": summary,
"key_points": key_points,
"org_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", # Your org
"role": "compliance_officer"
},
"state": "completed",
"execution_id": "summary-batch-001"
})
Step 4: Submit Results (POST)¶
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
-d '{
"results": [
{
"artifact_id": "abc123...",
"output_data": {
"summary": "The Capital Markets Authority has introduced new reporting requirements...",
"key_points": [
"Enhanced disclosure for institutional investors",
"Quarterly compliance certifications required",
"Effective date: January 1, 2026"
],
"org_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"role": "compliance_officer"
},
"state": "completed",
"execution_id": "summary-batch-001"
}
]
}' \
http://localhost:8000/api/v1/artifacts/dags/6527c9b7-25ee-4728-a237-aa3e8e62db94/results \
| python3 -m json.tool
What Happened Automatically:
1. ✅ New record inserted into dag_summaries table
2. ✅ summary_text = your summary
3. ✅ key_points = your key points array
4. ✅ organization_id = your org UUID
5. ✅ user_role = "compliance_officer"
6. ✅ crawl_outcome_id = linked to source document
Common Workflows¶
Workflow 1: Continuous Processing Loop¶
import time
import requests
DAG_ID = "your-dag-id"
API_BASE = "http://localhost:8000/api/v1/artifacts"
API_KEY = "your-api-key"
while True:
# 1. Fetch work items
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/work-items?limit=50",
headers={"X-API-Key": API_KEY}
)
work_items = response.json()["work_items"]
if len(work_items) == 0:
print("No work items available, sleeping...")
time.sleep(300) # Wait 5 minutes
continue
# 2. Process items
results = process_items(work_items) # Your processing logic
# 3. Submit results
requests.post(
f"{API_BASE}/dags/{DAG_ID}/results",
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
json={"results": results}
)
print(f"Processed {len(results)} items")
Workflow 2: Check DAG Stats¶
Monitor your DAG's progress:
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
http://localhost:8000/api/v1/artifacts/dags/153524a3-1e10-460c-b8a3-3ce431257058/stats \
| python3 -m json.tool
Response:
{
"dag_id": "153524a3-1e10-460c-b8a3-3ce431257058",
"total": 1250,
"pending": 0,
"sent_for_processing": 45,
"completed": 1200,
"failed": 5,
"retrying": 0
}
Workflow 3: Query Artifacts¶
View completed artifacts:
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
"http://localhost:8000/api/v1/artifacts/dags/153524a3-1e10-460c-b8a3-3ce431257058/artifacts?state=completed&limit=10" \
| python3 -m json.tool
Querying Artifacts with Positive & Negative Filters¶
The artifacts table supports comprehensive querying with both positive (match) and negative (exclude) filters.
Understanding Query Types¶
Positive Filters (Match)¶
# Get completed artifacts
?state=completed
# Get artifacts from crawl_outcomes
?source_table=crawl_outcomes
# Get topic-level artifacts
?source_kind=topic
Negative Filters (Exclude)¶
# Get all NON-completed artifacts
?state_not=completed
# Get artifacts NOT from crawl_outcomes
?source_table_not=crawl_outcomes
# Get all except record-level artifacts
?source_kind_not=record
List Filters (Multiple Values)¶
# Get pending OR sent_for_processing
?state_in=pending,sent_for_processing
# Get everything EXCEPT completed/failed
?state_not_in=completed,failed
# Get artifacts from multiple tables
?source_table_in=crawl_outcomes,feed_entries
Common Query Patterns¶
1. Find Unprocessed Items¶
# Items not yet completed
curl -H "X-API-Key: your-key" \
"http://localhost:8000/api/v1/artifacts/dags/{dag_id}/artifacts?state_not=completed"
2. Find Recently Completed Items¶
# Completed in last 24 hours
CUTOFF=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ')
curl -H "X-API-Key: your-key" \
"http://localhost:8000/api/v1/artifacts/dags/{dag_id}/artifacts?state=completed&completed_after=${CUTOFF}"
3. Find Stale Items¶
# Completed MORE than 24 hours ago
CUTOFF=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ')
curl -H "X-API-Key: your-key" \
"http://localhost:8000/api/v1/artifacts/dags/{dag_id}/artifacts?state=completed&completed_before=${CUTOFF}"
4. Find Failed Items with Errors¶
curl -H "X-API-Key: your-key" \
"http://localhost:8000/api/v1/artifacts/dags/{dag_id}/artifacts?state=failed&has_error=true"
5. Find Items by Source Kind¶
# Only topic-level aggregations
curl -H "X-API-Key: your-key" \
"http://localhost:8000/api/v1/artifacts/dags/{dag_id}/artifacts?source_kind=topic"
# Everything except record-level
curl -H "X-API-Key: your-key" \
"http://localhost:8000/api/v1/artifacts/dags/{dag_id}/artifacts?source_kind_not=record"
Python Query Examples¶
import requests
from datetime import datetime, timedelta
API_BASE = "http://localhost:8000/api/v1/artifacts"
API_KEY = "your-api-key"
DAG_ID = "your-dag-uuid"
# Example 1: Find all non-completed artifacts
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/artifacts",
params={"state_not": "completed", "limit": 100},
headers={"X-API-Key": API_KEY}
)
print(f"Found {len(response.json())} unfinished artifacts")
# Example 2: Find topic-level artifacts completed recently
cutoff = datetime.utcnow() - timedelta(hours=24)
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/artifacts",
params={
"source_kind": "topic",
"state": "completed",
"completed_after": cutoff.isoformat() + "Z",
"limit": 50
},
headers={"X-API-Key": API_KEY}
)
for artifact in response.json():
topic_info = artifact["source_metadata"]
print(f"Topic: {topic_info.get('topic_name')}, Items: {topic_info.get('item_count')}")
# Example 3: Find feed entries NOT yet processed by this DAG
# Step 1: Get all processed feed_entry IDs
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/artifacts",
params={
"source_table": "feed_entries",
"limit": 1000
},
headers={"X-API-Key": API_KEY}
)
processed_ids = {art["source_id"] for art in response.json()}
# Step 2: Query all feed_entries and filter out processed ones
all_entries = requests.get(
"http://localhost:8000/api/v1/feed-entries",
params={"limit": 1000},
headers={"X-API-Key": API_KEY}
).json()
unprocessed = [e for e in all_entries if e["id"] not in processed_ids]
print(f"Found {len(unprocessed)} unprocessed feed entries")
Example 3: Topic-Level Processing DAG¶
Use Case¶
You want to process all content related to a specific topic (e.g., "Malta Financial Regulations") as a batch, rather than individual documents. This is useful for: - Creating topic-level meta-summaries - Analyzing trends across all documents in a topic - Generating topic digests for users
Understanding Source Kind¶
When processing at topic-level, artifacts have:
- source_kind: "topic" (instead of default "record")
- source_metadata containing topic context
- Still reference individual records via source_id and source_table
Step 1: Register Topic-Level DAG¶
{
"name": "Topic Meta-Summary DAG",
"description": "Generates meta-summaries for entire topics",
"source_table": "crawl_outcomes",
"source_columns": ["id", "extracted_metadata", "feed_entry_id"],
"input_selector": {
"filter": {
"status": "completed",
"ranking_needed": true
},
"order_by": ["created_at"]
},
"output_schema": {
"topic_summary": "string",
"trending_themes": "array",
"urgency_level": "string",
"key_developments": "array"
},
"registered_actions": [
{
"action_type": "insert_into_table",
"target_table": "dag_summaries",
"mappings": [
{"output_key": "topic_summary", "target_column": "summary_text"},
{"output_key": "key_developments", "target_column": "key_points"},
{"source_column": "id", "target_column": "crawl_outcome_id"}
]
}
],
"config_metadata": {
"processing_mode": "topic_aggregation",
"aggregation_window": "7_days"
}
}
Register via API:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
-d @topic_meta_summary_dag.json \
http://localhost:8000/api/v1/artifacts/dags
Save the returned dag_id: 8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
Step 2: External System Creates Topic-Level Work Items¶
Your external system queries the database to group crawl_outcomes by topic and creates artifacts:
import requests
import psycopg2
API_BASE = "http://localhost:8000/api/v1/artifacts"
API_KEY = "jKA3nuqP1maUyeFTRX23ieYbezaynBR4"
DAG_ID = "8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
# Connect to database
conn = psycopg2.connect("dbname=regulatory_monitor user=reguser password=regpass123 host=localhost")
cur = conn.cursor()
# Get topics with their crawl outcomes
query = """
SELECT
t.id as topic_id,
t.name as topic_name,
array_agg(co.id) as crawl_outcome_ids,
count(*) as item_count
FROM topics t
JOIN rss_feeds rf ON rf.topic_id = t.id
JOIN feed_entries fe ON fe.feed_id = rf.id
JOIN crawl_outcomes co ON co.feed_entry_id = fe.id
WHERE co.status = 'completed'
AND co.ranking_needed = true
AND co.created_at > NOW() - INTERVAL '7 days'
GROUP BY t.id, t.name
HAVING count(*) >= 5 -- Only topics with 5+ items
"""
cur.execute(query)
topics = cur.fetchall()
print(f"Found {len(topics)} topics to process")
# For each topic, create artifacts manually via direct DB insert
# (In production, you'd extend the WorkItemService to support topic-level creation)
for topic_id, topic_name, crawl_ids, item_count in topics:
# For this example, create one artifact per crawl_outcome but mark it as topic-level
for crawl_id in crawl_ids:
# Fetch the crawl_outcome data
cur.execute("""
SELECT id, extracted_metadata
FROM crawl_outcomes
WHERE id = %s
""", (crawl_id,))
co_id, extracted_metadata = cur.fetchone()
# Create artifact with source_kind='topic' and source_metadata
cur.execute("""
INSERT INTO artifacts (
dag_id, source_kind, source_table, source_id,
source_metadata, input_data, state, dag_config_id
)
VALUES (
%s, 'topic', 'crawl_outcomes', %s,
%s, %s, 'sent_for_processing',
(SELECT id FROM dag_configurations WHERE dag_id = %s)
)
ON CONFLICT (dag_id, source_id) DO NOTHING
RETURNING id
""", (
DAG_ID,
co_id,
{
"topic_id": str(topic_id),
"topic_name": topic_name,
"item_count": item_count,
"aggregation_window": "7_days"
},
{
"id": str(co_id),
"extracted_metadata": extracted_metadata,
"topic_context": {
"topic_id": str(topic_id),
"topic_name": topic_name
}
},
DAG_ID
))
conn.commit()
print("Topic-level artifacts created")
Step 3: Fetch Topic-Level Work Items¶
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
"http://localhost:8000/api/v1/artifacts/dags/8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c/work-items?limit=10"
Response:
{
"dag_id": "8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"work_items": [
{
"artifact_id": "artifact-uuid-1",
"source_id": "crawl-outcome-uuid-1",
"dag_id": "8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"input_data": {
"id": "crawl-outcome-uuid-1",
"extracted_metadata": {
"title": "New Banking Directive",
"s3_content_md_path": "s3://bucket/content/abc123.md",
"url": "https://mfsa.mt/news/..."
},
"topic_context": {
"topic_id": "malta-topic-uuid",
"topic_name": "Malta Financial Regulations"
}
},
"created_at": "2025-12-08T10:00:00Z",
"sent_at": "2025-12-08T10:01:00Z"
}
],
"count": 10,
"cache_hit": true
}
Notice: Each work item includes topic_context in input_data.
Step 4: Process Topic-Level Items¶
import boto3
import requests
from collections import defaultdict
from openai import OpenAI
API_BASE = "http://localhost:8000/api/v1/artifacts"
API_KEY = "jKA3nuqP1maUyeFTRX23ieYbezaynBR4"
DAG_ID = "8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
client = OpenAI()
s3 = boto3.client('s3')
# Fetch work items
response = requests.get(
f"{API_BASE}/dags/{DAG_ID}/work-items?limit=50",
headers={"X-API-Key": API_KEY}
)
work_items = response.json()["work_items"]
# Group by topic
topics = defaultdict(list)
for item in work_items:
topic_id = item["input_data"]["topic_context"]["topic_id"]
topics[topic_id].append(item)
# Process each topic as a batch
results = []
for topic_id, items in topics.items():
topic_name = items[0]["input_data"]["topic_context"]["topic_name"]
print(f"Processing topic: {topic_name} ({len(items)} items)")
# Collect all content for this topic
all_content = []
for item in items:
s3_path = item["input_data"]["extracted_metadata"]["s3_content_md_path"]
# Parse S3 path
bucket, key = s3_path.replace("s3://", "").split("/", 1)
obj = s3.get_object(Bucket=bucket, Key=key)
content = obj['Body'].read().decode('utf-8')
all_content.append({
"title": item["input_data"]["extracted_metadata"]["title"],
"url": item["input_data"]["extracted_metadata"]["url"],
"content": content
})
# Generate meta-summary for entire topic
combined_text = "\n\n".join([
f"Title: {doc['title']}\nURL: {doc['url']}\n{doc['content']}"
for doc in all_content
])
response = client.chat.completions.create(
model="gpt-4",
messages=[{
"role": "system",
"content": f"You are analyzing regulatory updates for topic: {topic_name}. Generate a meta-summary covering key themes, developments, and urgency."
}, {
"role": "user",
"content": f"Analyze these {len(all_content)} documents and provide:\n1. Overall summary\n2. Trending themes\n3. Urgency level\n4. Key developments\n\nDocuments:\n{combined_text[:10000]}"
}],
response_format={"type": "json_object"}
)
meta_summary = response.choices[0].message.content
# Create results for each artifact (all get same meta-summary)
for item in items:
results.append({
"artifact_id": item["artifact_id"],
"output_data": meta_summary,
"state": "completed",
"execution_id": f"topic-meta-{topic_id[:8]}-2025-12-08"
})
print(f"Processed {len(results)} artifacts across {len(topics)} topics")
Step 5: Submit Results¶
# Submit all results
submit_response = requests.post(
f"{API_BASE}/dags/{DAG_ID}/results",
json={"results": results},
headers={"X-API-Key": API_KEY}
)
print(submit_response.json())
Response:
{
"processed": 50,
"failed": 0,
"action_results": [
{
"artifact_id": "artifact-uuid-1",
"actions": {
"actions_executed": 1,
"results": [
{
"success": true,
"action_type": "insert_into_table",
"target_table": "dag_summaries",
"summary_id": "summary-uuid-1",
"created": true
}
]
}
}
]
}
Step 6: Query Topic-Level Artifacts¶
# Get all topic-level artifacts
curl -H "X-API-Key: jKA3nuqP1maUyeFTRX23ieYbezaynBR4" \
"http://localhost:8000/api/v1/artifacts/dags/8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c/artifacts?source_kind=topic&state=completed"
What the Artifacts Table Looks Like:
┌──────────────────────────────────────────────────────────────────────────────┐
│ artifact_id │ abc123-... │
│ dag_id │ 8a9f2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c │
│ source_kind │ topic │
│ source_table │ crawl_outcomes │
│ source_id │ crawl-outcome-uuid-1 │
│ source_metadata │ { │
│ │ "topic_id": "malta-topic-uuid", │
│ │ "topic_name": "Malta Financial Regulations", │
│ │ "item_count": 15, │
│ │ "aggregation_window": "7_days" │
│ │ } │
│ state │ completed │
│ input_data │ { │
│ │ "id": "crawl-outcome-uuid-1", │
│ │ "extracted_metadata": {...}, │
│ │ "topic_context": { │
│ │ "topic_id": "malta-topic-uuid", │
│ │ "topic_name": "Malta Financial Regulations" │
│ │ } │
│ │ } │
│ output_data │ { │
│ │ "topic_summary": "Over past week, Malta MFSA...", │
│ │ "trending_themes": ["AML", "Licensing", "ESG"], │
│ │ "urgency_level": "high", │
│ │ "key_developments": [...] │
│ │ } │
│ created_at │ 2025-12-08T10:00:00Z │
│ completed_at │ 2025-12-08T10:15:00Z │
└──────────────────────────────────────────────────────────────────────────────┘
Key Differences from Record-Level:
1. source_kind = "topic" (not "record")
2. source_metadata contains topic context
3. Multiple artifacts can share same topic_id in metadata
4. Each still tracks individual source_id for traceability
Best Practices¶
1. Batch Processing¶
- Fetch 50-100 items at a time for optimal performance
- Don't request more than you can process in one execution
2. Error Handling¶
If an item fails to process, mark it as failed:
{
"artifact_id": "abc123...",
"state": "failed",
"error_message": "Content extraction failed: Invalid S3 path"
}
The system will track retry counts and you can retry failed items.
3. Execution IDs¶
Always provide a unique execution_id:
- Helps track which batch processed which items
- Written to ranking_execution_id for traceability
- Format: {service}-{date}-{batch_number} (e.g., "classification-2025-12-08-001")
4. Monitoring¶
- Check DAG stats regularly
- Monitor failed count
- Review error messages in artifacts table
5. Idempotency¶
- The system prevents duplicate processing via
(dag_id, source_id)uniqueness - If you fetch the same item twice, it returns the existing artifact
- Safe to retry work item fetches
Troubleshooting¶
Issue: No work items returned¶
Check:
1. Are there source records matching your input_selector.filter?
- Have all items already been processed?
Issue: Results submission fails¶
Common causes:
- Invalid artifact_id (typo or wrong UUID)
- Missing required fields in output_data
- output_data doesn't match output_schema
Solution: Check error response for details
Issue: Registered action fails¶
Common causes:
- target_column doesn't exist in target table
- Data type mismatch (e.g., trying to write string to integer column)
- Missing required columns in mappings
Solution: Check action_results in response for specific error
API Quick Reference¶
| Endpoint | Method | Purpose |
|---|---|---|
/artifacts/dags |
POST | Register new DAG |
/artifacts/dags |
GET | List all DAGs |
/artifacts/dags/{dag_id} |
GET | Get DAG details |
/artifacts/dags/{dag_id}/work-items |
GET | Fetch work items |
/artifacts/dags/{dag_id}/results |
POST | Submit results |
/artifacts/dags/{dag_id}/stats |
GET | Get DAG statistics |
/artifacts/dags/{dag_id}/artifacts |
GET | Query artifacts |
/artifacts/docs/action-types |
GET | Get action documentation |
/artifacts/docs/available-tables |
GET | Get available tables/columns |
Complete Example Scripts¶
See the examples/ directory for complete working scripts:
- classification_pipeline.py - Full classification example
- summarization_pipeline.py - Full summarization example
- batch_processor.py - Production-ready batch processing loop
Support¶
For questions or issues:
1. Check the API docs: http://localhost:8000/api/v1/docs
2. Review the design docs: docs/dag-artifacts-design.md
3. Contact the platform team
Last Updated: December 9, 2025 Version: 2.0 (Added query filters and topic-level processing)