MIA Task Lifecycle
A "MIA task" is a recurring AI report — a cron expression bound to a natural-language query. Lifecycle is shared between RTDB (canonical state) and Temporal (execution schedule).
Data shape
Stored at pivotAiAgentCrons/{companyId}/{taskId} in RTDB:
{
id: string, // ULID
companyId: string,
title: string, // user-facing
description?: string,
schedule: string, // human-readable ("Every Monday at 9am")
cronExpression: string, // standard 5-7 field cron
reportQuery: string, // the AI question
enabled: boolean, // false = Temporal schedule paused
userId?: string, // owner / email recipient
scheduleId?: string | null, // Temporal Schedule ID, null on failed create
createdAt?: number,
updatedAt?: number
}
The API response wraps this with two extra fields:
{
task: CronTaskDto,
temporalScheduled: boolean, // true = Temporal accepted the schedule
temporalError?: string // present iff temporalScheduled is false
}
temporalScheduled is not persisted — it's a per-response signal. The frontend uses it to surface a "retry sync" affordance when Temporal was unreachable at create/update time.
States
| State | Definition | RTDB record | Temporal schedule |
|---|---|---|---|
| Draft | In-flight, transient | being-written | not yet created |
| Active | Normal operating state | exists, enabled: true | exists, unpaused |
| Paused | User-disabled | exists, enabled: false | exists, paused |
| Orphan | Persisted but unscheduled | exists, scheduleId: null | absent |
Active self-edits (no state change): a PATCH that changes cronExpression, reportQuery, or title triggers a delete-and-recreate of the Temporal schedule but leaves the task in Active. The schedule's ID is rotated; the RTDB record's enabled stays true. Documented as a transition below, not as a separate state.
Transitions
Create — POST /ai-memory/cron-tasks
- Write to RTDB
pivotAiAgentCrons/{cid}/{taskId}. - Call
createTemporalSchedule()infunctions/modules/ai-memory/services/temporal-scheduler.service.ts. - If Temporal succeeds → response has
temporalScheduled: trueandscheduleIdis set on the task. - If Temporal fails → response has
temporalScheduled: falseandtemporalError: <message>. RTDB write is not rolled back.
Admin / isMiaAgent callers can set userId on behalf of another user; non-admin callers always get uid from their token. See Security Model.
Update — PATCH /ai-memory/cron-tasks/:taskId
- If
cronExpression,reportQuery, ortitlechanged → delete the old Temporal schedule and create a new one. - If only
enabledchanged → callpauseTemporalSchedule()(no delete/recreate). - RTDB always updates.
Delete — DELETE /ai-memory/cron-tasks/:taskId
- Delete Temporal schedule first (idempotent — "not found" is treated as success).
- Then remove the RTDB record.
Fire — Temporal cron trigger
When the cron expression matches the schedule's timezone:
- Temporal starts
scheduledReportWorkflowon thepivot-ai-agentqueue with the task args. - The workflow refreshes the Firebase token if expired (
getServiceTokenActivity). - It fetches company memory + user rules + timezone in parallel.
- Runs the standard Sonnet tool loop (same code path as interactive chat's data branch).
- Generates the email body via Haiku (1024 tokens).
- Calls
sendReportEmailActivityto deliver. - Workflow result is visible in Temporal Web UI.
Reconciliation
There's no automatic drift reconciliation today. The orphan state (RTDB record without a Temporal schedule) is fixable by an idempotent re-PATCH from the frontend, which retries the schedule creation.
TODO: see Reference: TODOs for the open reconciler item.