EOP Workflow
Overview
The K12 EOP workflow (formerly referred to as the "Annex" workflow) is an asynchronous job-based pipeline that accepts an emergency operations plan editing request, runs the K12 safety agent to produce edits, and persists telemetry & results for polling and retrieval.
Note: Tool IDs still contain "annex" for historical reasons (e.g.,
k12-annex-editing-kickoff). Do not change tool IDs in code — documentation uses the accurate EOP name.
This document expands on the workflow map and maps each workflow step to the concrete tool(s) that perform it.
Public tool endpoints (APIM)
All EOP editing requests go through the async toolchain exposed via the K12 APIM host.
- Base URL (K12 Azure Dev APIM):
https://dev-k12-aitools-t27p-apim.azure-api.net - Endpoint template:
POST /ai/api/tools/{toolId}/execute - Payload shape:
{ "data": { ... } } - Required headers (APIM):
Authorization: Bearer {access_token}Ocp-Apim-Subscription-Key: {subscription_key}
Note: Examples on this page use APIM subscription keys for quick curl snippets; for tenant-hosted automation and production workflows, prefer OAuth bearer tokens (client credentials) issued by the Azure Entra tenant. -
Content-Type: application/json
When calling a local dev server without APIM/auth, auth headers may be omitted.
1) Kickoff
- Tool ID:
k12-annex-editing-kickoff - Endpoint:
POST /ai/api/tools/k12-annex-editing-kickoff/execute - Purpose: Enqueues an EOP editing job and returns tracking identifiers.
Key request fields (inside data):
| Field | Required | Notes |
|---|---|---|
emergencyOperationPlanId | ⚠️ | Plan identifier to update. Organization context still requires emergencyOperationPlan.organizationId. |
emergencyOperationPlan | ⚠️ | Frontend-provided EOP object; id, emergencyOperationPlanId, planId, or eopId is normalized as the plan identifier. organizationId is used directly for district/organization details. |
emergencyOperationPlanConfigurationId | ⚠️ | Configuration-stage identifier. Organization context still requires emergencyOperationPlanConfiguration.organizationId; mutually exclusive with plan and Master Copy identifiers. |
emergencyOperationPlanConfiguration | ⚠️ | Frontend-provided configuration object; ID aliases are normalized as the configuration identifier. organizationId is used directly for district/organization details. |
eopMasterCopyId | ⚠️ | Master Copy identifier for pre-EOP template form editing. Organization context still requires emergencyOperationPlanMasterCopy.organizationId; supported by the legacy and scenario-specific EOP kickoff tools. |
emergencyOperationPlanMasterCopy | ⚠️ | Frontend-provided Master Copy object. Saved objects may provide an ID alias; organizationId is used directly for district/organization details. |
userId | ✅ | Requesting user (GUID or email). |
roleId | ❌ | Optional role/persona identifier retained for traceability. |
userTagIds | ❌ | Alias for selected role/tag IDs; may be an empty array while frontend tag filling is pending. |
roles, userTags, generalUserTags | ❌ | Frontend-provided role/tag objects or names. These override backend-resolved role tags for prompt context when present. |
form.field.content | ✅ | HTML/rich-text. Use "" when doing prompt-only edits. |
prompt | ❌ | Additional user instruction. |
sessionId | ❌ | Correlation id; generated if omitted. |
requestId | ❌ | Deterministic job id; generated if omitted. |
waitForResult | ❌ | true blocks until finished; default is async. |
Exactly one identity source must be provided: emergencyOperationPlanId/emergencyOperationPlan, emergencyOperationPlanConfigurationId/emergencyOperationPlanConfiguration, or eopMasterCopyId/emergencyOperationPlanMasterCopy. District/organization details are resolved only from the explicit organizationId on the selected request object; the workflow no longer discovers organization IDs indirectly from EOP, configuration, or Master Copy IDs.
Example (cURL — kickoff, async):
curl -X POST "$BASE_URL/ai/api/tools/k12-annex-editing-kickoff/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"emergencyOperationPlan": {"id": "plan-sample", "organizationId": "org-sample"}, "userId": "principal@example.edu", "roleId": "safety-coordinator", "form": {"field": {"content": "<p>...</p>"}}, "prompt": "Shorten to two bullets"}}'
Example response (accepted):
{
"requestId": "9fd2055d-42ab-4c29-bf0d-51bd8f7db3ec",
"sessionId": "0e1a5e3f-3a3c-4d1f-bbc6-0e76b1c4a8c7",
"status": "accepted"
}
2) Status polling
- Tool ID:
k12-annex-editing-status - Endpoint:
POST /ai/api/tools/k12-annex-editing-status/execute - Request shape:
{ "data": { "requestIds": ["..."] } }
Example (cURL — status poll):
curl -X POST "$BASE_URL/ai/api/tools/k12-annex-editing-status/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestIds": ["$REQUEST_ID"]}}'
Example response:
[
{
"requestId": "9fd2055d-42ab-4c29-bf0d-51bd8f7db3ec",
"sessionId": "0e1a5e3f-3a3c-4d1f-bbc6-0e76b1c4a8c7",
"status": "processing"
}
]
3) Result retrieval
- Tool ID:
k12-annex-editing-result - Endpoint:
POST /ai/api/tools/k12-annex-editing-result/execute
Example (cURL — result fetch):
curl -X POST "$BASE_URL/ai/api/tools/k12-annex-editing-result/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "$REQUEST_ID"}}'
Example response (success):
{
"requestId": "9fd2055d-42ab-4c29-bf0d-51bd8f7db3ec",
"sessionId": "0e1a5e3f-3a3c-4d1f-bbc6-0e76b1c4a8c7",
"status": "success",
"message": "<p>Edited annex text ...</p>",
"isEdit": true
}
4) Cancel job (optional)
- Tool ID:
k12-cancel-task-request - Endpoint:
POST /ai/api/tools/k12-cancel-task-request/execute
Example (cURL — cancel):
curl -X POST "$BASE_URL/ai/api/tools/k12-cancel-task-request/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "$REQUEST_ID"}}'
End-to-end steps (expanded)
-
Kickoff / normalize request — tool:
k12-insert-task-request-log(insert-task-request-tool.ts)- Normalizes
sessionId/requestId(generate deterministic UUIDs when missing). - Persists raw payload into
K12SAFETY.log_EOP_TaskRequests. - Public kickoff/status/result/cancel contracts are documented above in Public tool endpoints (APIM).
- Normalizes
-
Create initial monitoring entry — tool:
k12-insert-task-in-monitoring-table(insert-task-monitoring-tool.ts)- Writes a
processingrow intoK12SAFETY.log_EOP_TaskStatusand returnsstatusHistory.
- Writes a
-
Invoke the K12 Safety agent — step id:
k12_invoke_agent- Agent fetched via
getK12SafetyAgent()usesCore.AgentProfilesfor system prompt overrides and may apply LLM overrides.
- Agent fetched via
- The agent runs using the Azure AI Inference provider and returns an
agentResponse(text + raw + parsed payload).
-
Persist agent response — tool:
k12-update-task-request-response(update-task-request-response-tool.ts)- Writes
ResponseStatus,ResponseMessage,ResponseIsEdit, andResponsePayloadJsonback tolog_EOP_TaskRequests.
- Writes
-
Finalize status — tool:
k12-update-task-status(update-task-status-tool.ts)- Marks the monitoring entry
success(orfailed/erroron exceptions) inlog_EOP_TaskStatus.
- Marks the monitoring entry
Tool-level contracts
Each tool exposes a Zod inputSchema/outputSchema. You can import and validate against them in tests or when composing your own orchestrators. Example:
import { insertTaskRequestTool } from 'packages/domain-k12/src/insert-task-request-tool.ts';
// access the tool's Zod schemas
const { inputSchema, outputSchema } = insertTaskRequestTool;
// validate raw input before calling the execute helper (tests or scripts)
const parsed = inputSchema.parse({ taskName: 'K12 EOP Workflow', requestId: 'req-123', sessionId: 's-123', userId: 'u1', roleId: 'r1', form: { field: { content: '<p>...</p>' } } });
// tools provide `execute` to run the same code paths the API uses
const row = await insertTaskRequestTool.execute(parsed);
Fire-and-forget vs synchronous runs
- The kickoff tool supports both an async (default:
accepted) and blocking mode (waitForResult: true). - In async mode the tool enqueues the job (writes
log_Jobs) and returns tracking ids immediately. Usek12-annex-editing-statusork12-annex-editing-resultto poll for completion. - In sync mode the tool waits for the workflow to finish and returns full
successpayload orfailedinformation.
Runner API & schema (developer reference)
The kickoff runner exposes a reusable programmatic helper you can call from scripts or tests:
runAnnexWorkflowKickoff({ input, mastra? }) → returns either the accepted envelope or the synchronous success envelope.
Example TypeScript import:
import { runAnnexWorkflowKickoff } from 'packages/domain-k12/src/annex-editing-workflow-runner.ts';
import type { KickoffInput } from 'packages/domain-k12/src/annex-editing-kickoff-schemas.ts';
const res = await runAnnexWorkflowKickoff({ input: myKickoffPayload, mastra: runtime });
Key details:
- The runner verifies a registered workflow ID (
K12-annex-workflow) is available; if missing it throws an informative error listing available workflows. - It creates a Mastra
createRun()run and storesworkflowRunIdin SQL rows for correlating runtime state and log rows. - Duplicate
requestIdvalues are treated as idempotent: the runner returns a 400-like error (Duplicate requestId) when a prior job exists for thatrequestId.
The KickoffInput Zod schema enforces fields such as one identity (emergencyOperationPlan or emergencyOperationPlanConfiguration, including explicit organizationId for organization context), userId, roleId, and a form object (see annex-editing-kickoff-schemas.ts). Here is a minimal example payload:
{
"emergencyOperationPlan": {"id": "plan-123", "organizationId": "org-123"},
"userId": "principal@example.edu",
"roleId": "safety-coordinator",
"form": { "field": { "content": "<p>Update the evacuation steps...</p>" } },
"waitForResult": false
}
Useful SQL queries for troubleshooting
- Check job rows:
SELECT request_id, job_type, status, error, created_at, updated_at
FROM K12SAFETY.log_Jobs
WHERE request_id = '...';
- Inspect request log entries (most recent):
SELECT TOP (5) ID, TaskName, RequestID, SessionID, CreatedAt, ResponseStatus
FROM K12SAFETY.log_EOP_TaskRequests
WHERE RequestID = '...'
ORDER BY CreatedAt DESC;
- Get the status timeline:
SELECT TOP (10) ID, TaskName, RequestID, Status, Message, CreatedAt
FROM K12SAFETY.log_EOP_TaskStatus
WHERE RequestID = '...'
ORDER BY CreatedAt DESC;
Operational & troubleshooting tips
- If a run hangs, check both
K12SAFETY.log_Jobs(job runner state) andlog_EOP_TaskStatus(processing timeline). The workflow step that executed last is the best hint of the problem. - Use
k12-cancel-task-requestto ask the system to mark rowscancelledand stop in-progress runs (it updates DB rows; Mastra-level cancellation is best-effort unlessK12_ENABLE_WORKFLOW_CANCELLATIONis enabled). - For local debugging, import the EOP runner
runAnnexWorkflowKickofffromannex-editing-workflow-runner.tsto run the logic synchronously in tests.
Files to inspect
- Workflow:
packages/domain-k12/src/k12-annex.workflow.ts - Tools:
packages/domain-k12/src/*.ts - Agent:
packages/domain-k12/src/agents/k12-safety-agent.ts - Tests:
packages/domain-k12/tests/tools/,tests/integration/k12/,tests/shared/integration/workflows/k12-annex-schemas.test.ts