State Machine Engine

Entity State Graphs & State Machine

Every entity in the graph passes through a lifecycle of states, driven by email metadata signals. A generic state machine engine enforces valid transitions, triggers side effects, and provides full auditability.

Entity Type: Person

Person Lifecycle

A person is discovered the first time their email address appears in any metadata field. They become active when they send an email and transition to dormant after 30 days of silence. Re-engagement returns them to active from any state.

Person

Person State Machine

Discovered
Seen in metadata, no direct interaction
Active
Recent email activity detected
Dormant
No activity for 30+ days
Archived
No activity for 90+ days or manually archived
Transitions
DiscoveredActiveon email_sent
DiscoveredDormanton no_activity_30d
ActiveDormanton no_activity_30d
DormantActiveon re_engaged
DormantArchivedon no_activity_90d
DormantArchivedon manual_archive
ArchivedActiveon re_engaged
Person Transition Rules
FromToTrigger
discoveredactiveemail_sent
discovereddormantno_activity_30d
activedormantno_activity_30d
dormantactivere_engaged
dormantarchivedno_activity_90d
dormantarchivedmanual_archive
archivedactivere_engaged
Entity Type: Project

Project Lifecycle

Projects are inferred from thread clusters with shared participants and subject lines. They move through a lifecycle from proposal to active work, completion or staleness, and eventual archival.

Project

Project State Machine

Proposed
Initial discussion detected
Active
Ongoing collaboration with deliverables
Completed
Goal achieved, no further action
Stale
No activity for 60+ days
Archived
Closed or superseded
Transitions
ProposedActiveon first_deliverable
ActiveCompletedon goal_confirmed
ActiveStaleon no_activity_60d
StaleActiveon re_engaged
StaleArchivedon no_activity_90d
CompletedArchivedon auto_archive_30d
Project Transition Rules
FromToTrigger
proposedactivefirst_deliverable
activecompletedgoal_confirmed
activestaleno_activity_60d
staleactivere_engaged
stalearchivedno_activity_90d
completedarchivedauto_archive_30d
Entity Type: Thread

Thread Lifecycle

Threads model email conversations. They are active while messages arrive, become stale after 14 days of silence, and can be marked resolved when a decision or completion signal is detected. Resolved threads auto-archive after 30 days.

Thread

Thread State Machine

Active
Recent messages in thread
Stale
No new messages for 14 days
Resolved
Decision or completion detected
Archived
No activity for 60+ days
Transitions
ActiveStaleon no_activity_14d
ActiveResolvedon decision_detected
StaleActiveon new_reply
StaleResolvedon decision_detected
StaleArchivedon no_activity_60d
ResolvedArchivedon auto_archive_30d
Thread Transition Rules
FromToTrigger
activestaleno_activity_14d
activeresolveddecision_detected
staleactivenew_reply
staleresolveddecision_detected
stalearchivedno_activity_60d
resolvedarchivedauto_archive_30d
Generic Engine

State Machine Engine

A generic, type-safe state machine engine enforces valid transitions for all entity types. Each entity type declares its own state graph; the engine validates every transition, runs optional pre-condition checks, and emits audit events.

state-machine-engine.tstypescript
// Generic state machine engine. Each entity type declares its
// own state graph; the engine enforces valid transitions and
// emits events on every state change.

export type StateTransitionRule<S extends string, T extends string> = {
  from: S;
  to: S;
  trigger: T;
  validate?: (entity: object) => boolean | Promise<boolean>;
};

export class StateMachineEngine<S extends string, T extends string> {
  private rules: StateTransitionRule<S, T>[];
  private onTransition?: (from: S, to: S, trigger: T) => void;

  constructor(
    rules: StateTransitionRule<S, T>[],
    hooks?: { onTransition?: (from: S, to: S, trigger: T) => void },
  ) {
    this.rules = rules;
    this.onTransition = hooks?.onTransition;
  }

  async transition(
    entity: { state: S; id: string },
    trigger: T,
  ): Promise<{ state: S; applied: boolean }> {
    const rule = this.rules.find(
      (r) => r.from === entity.state && r.trigger === trigger,
    );

    if (!rule) {
      throw new Error(
        `Invalid transition: ${entity.state} cannot transition on "${trigger}"`,
      );
    }

    if (rule.validate) {
      const valid = await rule.validate(entity);
      if (!valid) {
        return { state: entity.state, applied: false };
      }
    }

    this.onTransition?.(entity.state, rule.to, trigger);

    return { state: rule.to, applied: true };
  }

  getAvailableTriggers(currentState: S): T[] {
    return this.rules
      .filter((r) => r.from === currentState)
      .map((r) => r.trigger);
  }
}

// Example: Person state machine configuration
export type PersonState = "discovered" | "active" | "dormant" | "archived";
export type PersonTrigger =
  | "email_received"
  | "email_sent"
  | "no_activity_30d"
  | "no_activity_90d"
  | "manual_archive"
  | "re_engaged";

export const PERSON_STATE_RULES: StateTransitionRule<PersonState, PersonTrigger>[] = [
  { from: "discovered", to: "active", trigger: "email_sent" },
  { from: "discovered", to: "dormant", trigger: "no_activity_30d" },
  { from: "active", to: "dormant", trigger: "no_activity_30d" },
  { from: "active", to: "active", trigger: "email_received", validate: () => true },
  { from: "active", to: "active", trigger: "email_sent", validate: () => true },
  { from: "dormant", to: "active", trigger: "re_engaged" },
  { from: "dormant", to: "archived", trigger: "no_activity_90d" },
  { from: "dormant", to: "archived", trigger: "manual_archive" },
  { from: "archived", to: "active", trigger: "re_engaged" },
];
Orchestration

Applying State Transitions

State transitions are triggered by the same pipeline events that drive graph updates. When a new email is processed, the pipeline evaluates each affected entity's current state and applies the appropriate transition.

state-orchestrator.tstypescript
// State orchestrator: called by the GraphUpdater after each
// email is processed. Evaluates state transitions for every
// entity touched by the event.

export class StateOrchestrator {
  private engines = new Map<string, StateMachineEngine<string, string>>();

  constructor() {
    this.engines.set("person", new StateMachineEngine(PERSON_STATE_RULES, {
      onTransition: (from, to, trigger) => {
        console.log(`Person ${from} -> ${to} via ${trigger}`);
      },
    }));
    // Project and Thread engines registered similarly
  }

  async evaluateTransitions(
    event: EmailMetadataEvent,
    entities: { person: Person[]; thread: Thread | null; project?: Project },
  ): Promise<StateChange[]> {
    const changes: StateChange[] = [];

    // Evaluate each person entity
    for (const person of entities.person) {
      const triggers = this.determinePersonTriggers(person, event);
      for (const trigger of triggers) {
        const engine = this.engines.get("person")!;
        try {
          const result = await engine.transition(
            { state: person.state, id: person.email },
            trigger,
          );
          if (result.applied) {
            changes.push({
              entityType: "person",
              entityId: person.email,
              fromState: person.state,
              toState: result.state,
              trigger,
              timestamp: Date.now(),
            });
            person.state = result.state;
          }
        } catch {
          // Invalid transition - log and skip
        }
      }
    }

    // Evaluate thread state
    if (entities.thread) {
      const threadTriggers = this.determineThreadTriggers(entities.thread, event);
      // ... same pattern for thread
    }

    return changes;
  }

  private determinePersonTriggers(
    person: Person,
    event: EmailMetadataEvent,
  ): string[] {
    const triggers: string[] = [];

    if (event.from === person.email) {
      triggers.push("email_sent");
    }
    if (event.to.includes(person.email) || event.cc.includes(person.email)) {
      triggers.push("email_received");
    }

    // Time-based triggers are evaluated by a background cron,
    // not in the hot path of email processing
    return triggers;
  }

  private determineThreadTriggers(
    thread: Thread,
    event: EmailMetadataEvent,
  ): string[] {
    const triggers: string[] = [];
    triggers.push("new_reply");

    if (event.extractedIntent?.type === "decision_record") {
      triggers.push("decision_detected");
    }

    return triggers;
  }
}

export type StateChange = {
  entityType: string;
  entityId: string;
  fromState: string;
  toState: string;
  trigger: string;
  timestamp: number;
};

// Time-based transitions run as a background cron, scanning
// entities where updated_at exceeds the threshold for the
// current state's next transition.
export async function evaluateTimeBasedTransitions(
  store: EntityStore,
  orchestrator: StateOrchestrator,
): Promise<StateChange[]> {
  const now = Date.now();
  const changes: StateChange[] = [];

  // Persons: discovered -> dormant after 30d, active -> dormant after 30d, dormant -> archived after 90d
  const thirtyDays = 30 * 86400_000;
  const ninetyDays = 90 * 86400_000;

  const stalePersons = await store.queryEntities("person", {
    filter: {
      anyOf: [
        { state: "discovered", updatedAt: { lt: now - thirtyDays } },
        { state: "active", updatedAt: { lt: now - thirtyDays } },
        { state: "dormant", updatedAt: { lt: now - ninetyDays } },
      ],
    },
  });

  for (const person of stalePersons) {
    const engine = orchestrator.getEngine("person")!;
    const trigger = person.state === "dormant" ? "no_activity_90d" : "no_activity_30d";
    const result = await engine.transition({ state: person.state, id: person.id }, trigger);
    if (result.applied) {
      changes.push({
        entityType: "person",
        entityId: person.id,
        fromState: person.state,
        toState: result.state,
        trigger,
        timestamp: now,
      });
    }
  }

  return changes;
}

MailGPT - Entity State Graphs & State Machine Specification

Generic state machine engine, entity lifecycle definitions, transition orchestration.