Skip to main content

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

StateDefinitionRTDB recordTemporal schedule
DraftIn-flight, transientbeing-writtennot yet created
ActiveNormal operating stateexists, enabled: trueexists, unpaused
PausedUser-disabledexists, enabled: falseexists, paused
OrphanPersisted but unscheduledexists, scheduleId: nullabsent

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

  1. Write to RTDB pivotAiAgentCrons/{cid}/{taskId}.
  2. Call createTemporalSchedule() in functions/modules/ai-memory/services/temporal-scheduler.service.ts.
  3. If Temporal succeeds → response has temporalScheduled: true and scheduleId is set on the task.
  4. If Temporal fails → response has temporalScheduled: false and temporalError: <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, or title changed → delete the old Temporal schedule and create a new one.
  • If only enabled changed → call pauseTemporalSchedule() (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:

  1. Temporal starts scheduledReportWorkflow on the pivot-ai-agent queue with the task args.
  2. The workflow refreshes the Firebase token if expired (getServiceTokenActivity).
  3. It fetches company memory + user rules + timezone in parallel.
  4. Runs the standard Sonnet tool loop (same code path as interactive chat's data branch).
  5. Generates the email body via Haiku (1024 tokens).
  6. Calls sendReportEmailActivity to deliver.
  7. 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.