Post-Quiz Processing Workflow — System Design Document
EduOne AI Platform | Version 1.0
| Field | Value |
|---|---|
| Author | EduOne Engineering Team |
| Created | 2026-07-17 |
| Status | Approved |
| Audience | Backend Engineers, DevOps, System Architects |
| Related Docs | Workflow Requirements Specification, API Reference |
Table of Contents
- Architecture & System Context
- Database Schema & State Management
- API Design
- Orchestrator Engine Logic
- Integration & Submission API Refactoring
- Error Handling, Transactions, & Recovery
1. Architecture & System Context
The Post-Quiz Processing subsystem orchestrates the operations that occur after a student submits a quiz. Due to the high computational demand of computing mastery updates, traversing the dependency graph, and generating recommendations, these tasks are offloaded to an asynchronous pipeline.
1.1 Cloudflare Workflows Integration
This system utilizes Cloudflare Workflows (a stateful, durable execution framework built on Cloudflare Workers) to act as the primary orchestrator. Cloudflare Workflows provides automatic checkpointing, state serialization, and built-in retries.
The orchestrator sits between the HTTP router (submission.ts) and the database/engine layers.
graph TB
subgraph Client ["Client Layer"]
STU["Student App (React)"]
end
subgraph API_GW ["Cloudflare Worker (Hono)"]
SUB_ROUTE["Submission Router<br/>/api/submissions"]
WF_ROUTE["Workflow Router<br/>/api/workflows"]
end
subgraph Orchestration ["Stateful Orchestrator"]
CF_WF["Cloudflare Workflows<br/>(PostQuizProcessingWorkflow)"]
end
subgraph Engines ["Domain Engines"]
GE["Grading Engine"]
EP["Evidence Processor"]
MC["Mastery Calculator"]
DGE["Dependency Graph Engine"]
RE["Recommendation Engine"]
end
subgraph Storage ["Data Layer"]
D1["Cloudflare D1<br/>(SQLite Database)"]
end
%% Flow lines
STU -->|1. Submit Answers| SUB_ROUTE
SUB_ROUTE -->|2. Create Submission (pending)| D1
SUB_ROUTE -->|3. Dispatch Workflow| CF_WF
SUB_ROUTE -->>STU|4. 202 Accepted (submissionId, workflowId)|
CF_WF -->|5. Run Step: Grade| GE
CF_WF -->|6. Run Step: Evidence| EP
CF_WF -->|7. Run Step: Mastery| MC
CF_WF -->|8. Run Step: Diagnostics| DGE
CF_WF -->|9. Run Step: Recommendation| RE
GE & EP & MC & DGE & RE -->|Read / Write State| D1
STU -->|10. Poll Status / Results| SUB_ROUTE
STU -->|11. Query Logs / Engine State| WF_ROUTE
WF_ROUTE -->|Read Workflow Instance| CF_WF
WF_ROUTE -->|Read Workflow Table| D11.2 Step Execution Flow
The workflow is broken down into 5 sequential steps, with the orchestrator writing checkpoints to the DB after each step completes.
sequenceDiagram
autonumber
participant Client
participant Hono as Hono Router
participant WF as Cloudflare Workflows
participant D1 as D1 Database
Client->>Hono: POST /api/submissions
Hono->>D1: Insert Submission (status='submitted')
Hono->>WF: Start PostQuizProcessingWorkflow (params)
Hono-->>Client: 202 Accepted (submission_id, workflow_id)
Note over WF: Step 1: Grade Quiz
WF->>D1: Fetch answers & scoring matrix
WF->>D1: Update lrn_submissions (score, status='graded')
Note over WF: Step 2: Evidence Generation
WF->>D1: Insert evd_evidence records (processed=0)
Note over WF: Step 3: Bayesian Mastery Updates
WF->>D1: Fetch old skill mastery states
WF->>D1: Compute new mastery (Weighted Moving Average)
WF->>D1: Update evd_learner_skill_states & evd_learner_models
Note over WF: Step 4: Prerequisite Gap Diagnosis
WF->>D1: Traverse dependency graph (cur_skill_dependencies)
WF->>WF: Isolate gaps where mastery < 0.7
Note over WF: Step 5: Recommendations
WF->>D1: Filter candidate content items
WF->>D1: Rank items and select top match
WF->>D1: Insert rec_recommendations record
Note over WF: Workflow Completes
WF->>D1: Update ops_workflows (status='completed')2. Database Schema & State Management
To track asynchronous operations, manage checkpoint state, and allow manual intervention/resumption, we expand the D1 schema with execution tables.
2.1 Schema Updates & Table Definitions
We introduce ops_workflow_steps to track step executions, and ops_workflow_context to hold variable states between steps.
erDiagram
ops_workflows ||--|{ ops_workflow_steps : "contains"
ops_workflows ||--|| ops_workflow_context : "stores snapshot of"
iam_organizations ||--|{ ops_workflows : "owns"
lrn_submissions ||--o| ops_workflows : "triggered by"
ops_workflows {
text id PK
text organization_id FK
text workflow_type
text trigger_type
text trigger_id
text status
integer priority
text error_message
text error_details
integer retry_count
integer max_retries
text started_at
text completed_at
text created_at
text updated_at
}
ops_workflow_steps {
text id PK
text workflow_id FK
text step_name
text status
text input_payload
text output_payload
text error_message
integer retry_count
text started_at
text completed_at
}
ops_workflow_context {
text workflow_id PK, FK
text context_json
text updated_at
}2.2 Complete SQL Migration DDL
The following SQL statements are applied to the D1 SQLite database to register the new tables and indexes.
-- Migration: Workflow System Schema
-- Target: Cloudflare D1 (SQLite)
-------------------------------------------------------
-- 1. Extend ops_workflows (if required, or defined fully)
-------------------------------------------------------
CREATE TABLE IF NOT EXISTS ops_workflows (
id TEXT NOT NULL PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES iam_organizations(id) ON DELETE RESTRICT,
workflow_type TEXT NOT NULL CHECK (workflow_type IN ('evidence_processing','model_update','recommendation_generation','content_review','quiz_grading','report_generation')),
trigger_type TEXT NOT NULL CHECK (trigger_type IN ('submission','schedule','manual','webhook','system')),
trigger_id TEXT, -- ID of the triggering entity (e.g. submission_id)
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','completed','failed','cancelled','retrying')),
priority INTEGER NOT NULL DEFAULT 5,
input_data TEXT, -- JSON: workflow input parameters
output_data TEXT, -- JSON: workflow results
error_message TEXT,
error_details TEXT, -- JSON: stack trace, debug info
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 3,
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
) STRICT;
CREATE INDEX IF NOT EXISTS idx_ops_wf_org ON ops_workflows(organization_id);
CREATE INDEX IF NOT EXISTS idx_ops_wf_type ON ops_workflows(workflow_type);
CREATE INDEX IF NOT EXISTS idx_ops_wf_status ON ops_workflows(status);
CREATE INDEX IF NOT EXISTS idx_ops_wf_trigger ON ops_workflows(trigger_type, trigger_id);
-------------------------------------------------------
-- 2. Step execution tracking table
-------------------------------------------------------
CREATE TABLE IF NOT EXISTS ops_workflow_steps (
id TEXT NOT NULL PRIMARY KEY,
workflow_id TEXT NOT NULL REFERENCES ops_workflows(id) ON DELETE CASCADE,
step_name TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed')),
input_payload TEXT, -- JSON: input parameters sent to this step
output_payload TEXT, -- JSON: output returned by this step
error_message TEXT, -- Error description if step failed
retry_count INTEGER NOT NULL DEFAULT 0,
started_at TEXT NOT NULL DEFAULT (datetime('now')),
completed_at TEXT
) STRICT;
CREATE INDEX IF NOT EXISTS idx_ops_wf_steps_wf ON ops_workflow_steps(workflow_id);
CREATE INDEX IF NOT EXISTS idx_ops_wf_steps_status ON ops_workflow_steps(workflow_id, status);
-------------------------------------------------------
-- 3. Workflow Context Snapshot table
-- Holds the serialized variables state between steps
-------------------------------------------------------
CREATE TABLE IF NOT EXISTS ops_workflow_context (
workflow_id TEXT NOT NULL PRIMARY KEY REFERENCES ops_workflows(id) ON DELETE CASCADE,
context_json TEXT NOT NULL, -- JSON representation of current variables
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
) STRICT;2.3 State Machine Transition Matrix
The ops_workflows table tracks the lifecycle of the entire processing pipeline. The allowed state transitions are:
| Initial State | Event Trigger | Next State | Action Taken |
|---|---|---|---|
pending | start | running | Initialize workflow context and execute the first step. |
running | step_success | running | Save step checkpoint to DB and advance current_step. |
running | step_fail (retry < max) | retrying | Log error, trigger exponential backoff. |
retrying | retry_timeout | running | Increment retry_count, re-execute the active step. |
running | step_fail (retry >= max) | failed | Log final exception, write error detail trace, alert admin. |
running | all_steps_complete | completed | Write final output parameters, update submission state to graded. |
failed | manual_resume | running | Reset step retry count, reload variables from context snapshot, execute failed step. |
running | cancel_signal | cancelled | Stop current step execution context and mark run as aborted. |
3. API Design
The system implements API routes under /api/workflows to monitor and manage workflow executions.
3.1 Routing Table
| Route | HTTP Method | Auth Role | Description |
|---|---|---|---|
/api/submissions | POST | student | Submit answers and trigger post-quiz processing workflow. |
/api/submissions/:id/results | GET | student, teacher | Retrieve grading results, mastery updates, and recommendations. |
/api/workflows/:id/status | GET | admin, teacher | Retrieve details of a workflow instance, its status, and step checkpoints. |
/api/workflows/:id/logs | GET | admin | Retrieve stdout/stderr execution logs and stack traces. |
/api/workflows/:id/resume | POST | admin | Manually resume a failed workflow execution from its last checkpoint. |
3.2 Endpoint Specifications
3.2.1 Trigger Submission Pipeline
- Endpoint:
POST /api/submissions - Request Payload:
{
"quiz_id": "qz_8A3f9c",
"class_id": "cl_2B8d4k",
"started_at": "2026-07-17T16:50:00Z",
"answers": [
{
"question_id": "q_111",
"selected_option_id": "opt_111_a",
"answer_text": null
},
{
"question_id": "q_222",
"selected_option_id": "opt_222_c",
"answer_text": null
}
]
}- Response Payload (202 Accepted):
{
"status": "accepted",
"message": "Submission received and queued for processing.",
"submission_id": "sub_4F9e2a8k",
"workflow_id": "wf_post_quiz_9D3j5s",
"urls": {
"status": "/api/workflows/wf_post_quiz_9D3j5s/status",
"results": "/api/submissions/sub_4F9e2a8k/results"
}
}3.2.2 Fetch Workflow Engine Status
- Endpoint:
GET /api/workflows/:id/status - Response Payload (200 OK):
{
"workflow_id": "wf_post_quiz_9D3j5s",
"type": "evidence_processing",
"trigger": {
"type": "submission",
"id": "sub_4F9e2a8k"
},
"status": "running",
"priority": 5,
"progress": {
"total_steps": 5,
"completed_steps": 2,
"current_step": "Calculate Mastery"
},
"steps": [
{
"step_name": "Grade Submission",
"status": "completed",
"retry_count": 0,
"started_at": "2026-07-17T17:00:01Z",
"completed_at": "2026-07-17T17:00:03Z"
},
{
"step_name": "Process Evidence",
"status": "completed",
"retry_count": 0,
"started_at": "2026-07-17T17:00:03Z",
"completed_at": "2026-07-17T17:00:04Z"
},
{
"step_name": "Calculate Mastery",
"status": "running",
"retry_count": 1,
"started_at": "2026-07-17T17:00:04Z",
"completed_at": null
},
{
"step_name": "Run Gap Diagnosis",
"status": "pending",
"retry_count": 0,
"started_at": null,
"completed_at": null
},
{
"step_name": "Generate Recommendation",
"status": "pending",
"retry_count": 0,
"started_at": null,
"completed_at": null
}
],
"created_at": "2026-07-17T17:00:00Z",
"updated_at": "2026-07-17T17:00:06Z"
}3.2.3 Fetch Execution Logs
- Endpoint:
GET /api/workflows/:id/logs - Response Payload (200 OK):
{
"workflow_id": "wf_post_quiz_9D3j5s",
"logs": [
{
"timestamp": "2026-07-17T17:00:00Z",
"level": "info",
"message": "Workflow PostQuizProcessing initialized with submission ID: sub_4F9e2a8k."
},
{
"timestamp": "2026-07-17T17:00:01Z",
"level": "info",
"message": "Step 'Grade Submission' started."
},
{
"timestamp": "2026-07-17T17:00:03Z",
"level": "info",
"message": "Step 'Grade Submission' finished successfully. Score: 8/10."
},
{
"timestamp": "2026-07-17T17:00:03Z",
"level": "info",
"message": "Step 'Process Evidence' started."
},
{
"timestamp": "2026-07-17T17:00:04Z",
"level": "warn",
"message": "Anomaly detected in question q_222: time spent 1.1s is below threshold 1.5s. Weight adjusted to 0.2."
},
{
"timestamp": "2026-07-17T17:00:04Z",
"level": "info",
"message": "Step 'Calculate Mastery' failed. DB execution interrupted: D1 connection timeout. Retrying."
}
]
}3.2.4 Manual Resume Execution
- Endpoint:
POST /api/workflows/:id/resume - Request Payload:
{}(Empty JSON object) - Response Payload (200 OK):
{
"status": "resumed",
"workflow_id": "wf_post_quiz_9D3j5s",
"resumed_from_step": "Calculate Mastery",
"timestamp": "2026-07-17T17:05:00Z"
}4. Orchestrator Engine Logic
The orchestrator logic is implemented using the Cloudflare Workflows API, utilizing the WorkflowEntrypoint class to create step boundaries.
4.1 Orchestrator Engine Implementation (TypeScript)
import { WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
import { generateId } from '../utils/id';
// Define context payload interface
export interface PostQuizWorkflowParams {
submissionId: string;
orgId: string;
userId: string;
classId: string;
courseId: string;
}
export class PostQuizProcessingWorkflow extends WorkflowEntrypoint<PostQuizWorkflowParams> {
async run(event: any, env: any) {
const { submissionId, orgId, userId, classId, courseId } = this.params;
const db = env.DB;
const workflowId = this.id;
// Helper to log and track step execution state in SQL database
const updateStepStatus = async (
stepName: string,
status: 'pending' | 'running' | 'completed' | 'failed',
input: any = null,
output: any = null,
errorMsg: string | null = null
) => {
const now = new Date().toISOString();
const stepId = `${workflowId}_${stepName.replace(/\s+/g, '_').toLowerCase()}`;
await db.prepare(`
INSERT INTO ops_workflow_steps (id, workflow_id, step_name, status, input_payload, output_payload, error_message, started_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
status = excluded.status,
output_payload = COALESCE(excluded.output_payload, output_payload),
error_message = COALESCE(excluded.error_message, error_message),
completed_at = CASE WHEN excluded.status IN ('completed', 'failed') THEN ? ELSE completed_at END
`).bind(
stepId,
workflowId,
stepName,
status,
input ? JSON.stringify(input) : null,
output ? JSON.stringify(output) : null,
errorMsg,
now,
now
).run();
};
// Update workflow context in D1
const saveContextSnapshot = async (contextData: any) => {
await db.prepare(`
INSERT INTO ops_workflow_context (workflow_id, context_json, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(workflow_id) DO UPDATE SET
context_json = excluded.context_json,
updated_at = excluded.updated_at
`).bind(workflowId, JSON.stringify(contextData), new Date().toISOString()).run();
};
// Initialize workflow run metadata
await db.prepare(`
INSERT INTO ops_workflows (id, organization_id, workflow_type, trigger_type, trigger_id, status, started_at)
VALUES (?, ?, 'evidence_processing', 'submission', ?, 'running', ?)
`).bind(workflowId, orgId, submissionId, new Date().toISOString()).run();
let context: any = { submissionId, orgId, userId, classId, courseId };
await saveContextSnapshot(context);
// ============================================================
// STEP 1: Objective Grading
// ============================================================
const gradingResults = await this.step.run('Grade Submission', {
retries: {
limit: 3,
delay: '2s',
backoff: 'exponential'
},
timeout: '30s'
}, async () => {
await updateStepStatus('Grade Submission', 'running', { submissionId });
try {
// Execute scoring calculations
// (Simulated engine call; in production imports actual logic)
const quizSubmission = await db.prepare(
`SELECT q.id as quiz_id, s.started_at
FROM lrn_submissions s
JOIN lrn_quizzes q ON q.id = s.quiz_id
WHERE s.id = ?`
).bind(submissionId).first();
if (!quizSubmission) {
throw new Error(`Submission ${submissionId} not found`);
}
// Return grading parameters to output checkpoint
const result = {
gradedAt: new Date().toISOString(),
status: 'success'
};
await updateStepStatus('Grade Submission', 'completed', null, result);
return result;
} catch (err: any) {
await updateStepStatus('Grade Submission', 'failed', null, null, err.message);
throw err;
}
});
context.gradingResults = gradingResults;
await saveContextSnapshot(context);
// ============================================================
// STEP 2: Evidence Generation
// ============================================================
const evidenceResults = await this.step.run('Process Evidence', {
retries: { limit: 3, delay: '2s', backoff: 'exponential' }
}, async () => {
await updateStepStatus('Process Evidence', 'running', { submissionId });
try {
// Load submission answers to generate evidence
const { results: answers } = await db.prepare(`
SELECT id, question_id, score, is_correct
FROM lrn_submission_answers
WHERE submission_id = ?
`).bind(submissionId).all();
// Process logic... (anomaly checks, normalization)
const processedEvidenceIds: string[] = [];
for (const answer of answers) {
// Identify indicator mapping
const question = await db.prepare(
`SELECT indicator_id FROM lrn_questions WHERE id = ?`
).bind(answer.question_id).first();
if (question?.indicator_id) {
processedEvidenceIds.push(answer.id);
}
}
const result = { processedEvidenceIds, count: processedEvidenceIds.length };
await updateStepStatus('Process Evidence', 'completed', null, result);
return result;
} catch (err: any) {
await updateStepStatus('Process Evidence', 'failed', null, null, err.message);
throw err;
}
});
context.evidenceResults = evidenceResults;
await saveContextSnapshot(context);
// ============================================================
// STEP 3: Bayesian Mastery Updates
// ============================================================
const masteryResults = await this.step.run('Calculate Mastery', {
retries: { limit: 3, delay: '2s', backoff: 'exponential' }
}, async () => {
await updateStepStatus('Calculate Mastery', 'running', { userId });
try {
// Recalculate states using weighted average
const result = {
updatedSkills: ['sk_math_matrices'],
timestamp: new Date().toISOString()
};
await updateStepStatus('Calculate Mastery', 'completed', null, result);
return result;
} catch (err: any) {
await updateStepStatus('Calculate Mastery', 'failed', null, null, err.message);
throw err;
}
});
context.masteryResults = masteryResults;
await saveContextSnapshot(context);
// ============================================================
// STEP 4: Gap Diagnosis
// ============================================================
const diagnosticGaps = await this.step.run('Run Gap Diagnosis', {
retries: { limit: 3, delay: '2s', backoff: 'exponential' }
}, async () => {
await updateStepStatus('Run Gap Diagnosis', 'running', { userId });
try {
// Diagnose prerequisite gaps in dependency graph
const gaps = ['sk_algebra_foundations'];
await updateStepStatus('Run Gap Diagnosis', 'completed', null, { gaps });
return { gaps };
} catch (err: any) {
await updateStepStatus('Run Gap Diagnosis', 'failed', null, null, err.message);
throw err;
}
});
context.diagnosticGaps = diagnosticGaps;
await saveContextSnapshot(context);
// ============================================================
// STEP 5: Generate Recommendations
// ============================================================
const recResults = await this.step.run('Generate Recommendation', {
retries: { limit: 3, delay: '2s', backoff: 'exponential' }
}, async () => {
await updateStepStatus('Generate Recommendation', 'running', { userId });
try {
// Invoke candidate filters, rankers and reasons generators
const recommendationId = generateId('rec');
const result = { recommendationId, status: 'generated' };
await updateStepStatus('Generate Recommendation', 'completed', null, result);
return result;
} catch (err: any) {
await updateStepStatus('Generate Recommendation', 'failed', null, null, err.message);
throw err;
}
});
context.recResults = recResults;
await saveContextSnapshot(context);
// Mark overall workflow execution as completed successfully
await db.prepare(`
UPDATE ops_workflows
SET status = 'completed', completed_at = ?, updated_at = ?, output_data = ?
WHERE id = ?
`).bind(
new Date().toISOString(),
new Date().toISOString(),
JSON.stringify(context),
workflowId
).run();
return context;
}
}4.2 Checkpoint Preservation & Engine Details
The Cloudflare Workflows platform implements implicit state checkpointing. When step.run finishes, the return value is serialized and written to persistent Cloudflare state storage.
If the instance crashes during step 3:
- Steps 1 and 2 will not execute again because their serialized return values exist in the workflow history.
- The orchestrator resumes execution at step 3, passing the cached values of steps 1 and 2 to variables
gradingResultsandevidenceResults.
5. Integration & Submission API Refactoring
The synchronous path in routes/submission.ts is split at the grading boundary.
5.1 Before/After Workflow Boundaries
[BEFORE - Synchronous Route Processing]
Client POST ➔ Validate ➔ Grade ➔ Write Evidence ➔ Calculate Mastery ➔ Generate Rec ➔ Respond (201)
[AFTER - Asynchronous Orchestration]
Client POST ➔ Validate ➔ Create DB Submission (Status: submitted)
➔ Dispatch Cloudflare Workflow (PostQuizProcessingWorkflow)
➔ Return 202 Accepted immediately5.2 API Integration Diff Example
Here is how worker/src/routes/submission.ts is refactored. The database execution remains transactionally safe by changing the initial insert status.
- // 8. Process evidence
- const evidenceInputs = scoredAnswers.map((sa) => ({
- submissionId,
- questionId: sa.questionId,
- score: sa.score,
- maxScore: sa.points,
- answeredAt: now,
- timeTakenSeconds: timePerQuestion,
- }));
-
- const evidenceRecords = await processEvidence(
- db, orgId, user.id, class_id, evidenceInputs,
- );
-
- // 9. Calculate mastery updates
- const masteryUpdates = await calculateMastery(
- db, orgId, user.id, class_id, submissionId, evidenceRecords,
- );
-
- // 10. Generate new recommendation
- const classInfo = await db
- .prepare(`SELECT course_id FROM lrn_classes WHERE id = ?`)
- .bind(class_id)
- .first<{ course_id: string }>();
-
- let newRecommendation = null;
- if (classInfo) {
- const recResult = await generateRecommendation(
- db, orgId, user.id, class_id, classInfo.course_id,
- );
- newRecommendation = recResult.recommendation;
- }
-
- // Update submission status to graded
- await db
- .prepare(
- `UPDATE lrn_submissions SET status = 'graded', graded_at = ?, updated_at = ? WHERE id = ?`,
- )
- .bind(now, now, submissionId)
- .run();
-
- // 11. Build response
- // ... Return full payload with mastery updates
+ // 8. Dispatch Asynchronous Cloudflare Workflow
+ let workflowInstance;
+ try {
+ // Start execution on Cloudflare Workflows binding
+ workflowInstance = await c.env.WORKFLOWS.post_quiz_processing.create({
+ params: {
+ submissionId,
+ orgId,
+ userId: user.id,
+ classId: class_id,
+ courseId: classInfo?.course_id ?? ''
+ }
+ });
+ } catch (wfError: any) {
+ // Fallback: log error and queue submission in pending state
+ console.error('Failed to trigger workflow, queueing manually:', wfError);
+ }
+
+ // 9. Build and return 202 Accepted response immediately
+ const response = {
+ status: "accepted",
+ message: "Bài làm đã được ghi nhận và đang được chấm tự động.",
+ submission: {
+ id: submissionId,
+ attempt_number: attemptNo,
+ status: "submitted"
+ },
+ workflow: {
+ id: workflowInstance?.id ?? null,
+ status: "queued"
+ }
+ };
+
+ return c.json(response, 202);6. Error Handling, Transactions, & Recovery
Asynchronous execution introduces challenges with concurrency, locking, and data consistency.
6.1 D1 Transaction Boundaries
Cloudflare D1 runs SQLite under the hood. SQLite locks the entire database file during write transactions. To prevent transaction locks and timeouts:
- Keep Transactions Short: Do not bundle the entire workflow into a single SQLite transaction.
- Step Isolation: Each step executes separate, isolated transactions. Step 2 processes and commits its inserts before Step 3 runs its query.
- Idempotence with
INSERT OR IGNORE: If a step crashes halfway and gets retried, the query uses unique constraints (e.g.PRIMARY KEY(id)orUNIQUE(submission_id, question_id)) withINSERT OR IGNORE(orUPSERT) to prevent double-inserting.
6.2 Resumability Procedure
When a workflow fails, it transitions to failed state. The manual recovery workflow functions as follows:
stateDiagram-v2
StateFailed: Workflow Status = 'failed'
StateLoading: Reload Context from ops_workflow_context
StateResume: Resume from Failed Step
StateRunning: Status = 'running'
[*] --> StateFailed
StateFailed --> StateLoading: Administrator triggers /resume API
StateLoading --> StateResume: Resolve DB lock / code bug
StateResume --> StateRunning: Re-execute step logic
StateRunning --> [*]: Workflow Completes- Verification: The administrator queries the error details via
/api/workflows/:id/logs. - Context Resolution: The administrator POSTs to
/api/workflows/:id/resume. - Re-trigger: Hono matches the failed workflow instance ID and calls:typescript
const instance = await env.WORKFLOWS.post_quiz_processing.get(id); await instance.resume(); - Resumption: The Cloudflare Workflows runtime re-reads the serialized step context, skips the completed checkpoints, and starts execution directly from the step that failed.