Skip to main content
OmniEHR provides comprehensive clinical workflow management with task orchestration, risk stratification, care gap identification, and a centralized command center for population health monitoring.

Overview

Task Management

Create, assign, and track care coordination tasks with priority levels and due dates

Command Center

Enterprise operations dashboard for risk monitoring and population health

Care Gaps

Automated detection of quality opportunities and preventive care needs

Risk Stratification

Patient risk scoring based on conditions, medications, and clinical indicators

Task Management

OmniEHR implements FHIR Task resources for care coordination workflows.

Task Data Model

Task Schema
const taskSchema = new mongoose.Schema({
  resourceType: {
    type: String,
    default: "Task"
  },
  status: {
    type: String,
    enum: [
      "draft", "requested", "received", "accepted", "rejected",
      "ready", "cancelled", "in-progress", "on-hold", "failed",
      "completed", "entered-in-error"
    ],
    default: "requested"
  },
  intent: {
    type: String,
    default: "order"
  },
  priority: {
    type: String,
    enum: ["routine", "urgent", "asap", "stat"],
    default: "routine"
  },
  codeText: String,
  description: {
    type: String,
    required: true
  },
  for: {
    reference: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Patient",
      required: true
    }
  },
  ownerUserId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "User"
  },
  ownerName: String,
  authoredOn: Date,
  dueDate: Date,
  note: String,
  createdBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "User"
  }
}, {
  timestamps: true
});

// Indexes for efficient querying
taskSchema.index({ "for.reference": 1, dueDate: 1 });
taskSchema.index({ ownerUserId: 1, status: 1, dueDate: 1 });

Task Categories

Task Categories
const TASK_CATEGORY_OPTIONS = [
  "Care coordination",
  "Medication reconciliation",
  "Lab follow-up",
  "Preventive screening",
  "Discharge outreach",
  "Referral management"
];

Task Priority Levels

PriorityDescriptionUse Case
routineStandard priorityRegular follow-ups, routine care
urgentElevated priorityTime-sensitive coordination
asapAs soon as possibleCritical but not emergency
statImmediate attentionEmergency clinical needs

Creating Tasks

const onCreateTask = async (event) => {
  event.preventDefault();

  const ownerId = user.role === "practitioner" ? user.id : taskForm.ownerId;
  const owner = practitionerMap.get(ownerId);
  const dueIso = taskForm.dueDate
    ? new Date(`${taskForm.dueDate}T23:59:59`).toISOString()
    : undefined;

  await fhirApi.createTask(token, {
    resourceType: "Task",
    status: "requested",
    intent: "order",
    priority: taskForm.priority,
    code: taskForm.category ? { text: taskForm.category } : undefined,
    description: taskForm.description.trim(),
    for: {
      reference: `Patient/${patientId}`
    },
    owner: ownerId ? {
      reference: `Practitioner/${ownerId}`,
      display: owner?.fullName
    } : undefined,
    authoredOn: new Date().toISOString(),
    executionPeriod: dueIso ? { end: dueIso } : undefined,
    note: taskForm.note ? [{ text: taskForm.note.trim() }] : undefined
  });

  await load();
};

Task Status Workflow

Task Status Update
const onUpdateTaskStatus = async (task, nextStatus) => {
  await submitClinicalResource({
    request: () =>
      fhirApi.updateTask(token, task.id, {
        ...task,
        status: nextStatus
      }),
    onSuccess: () => {}
  });
};

// Render status dropdown
<select
  value={task.status}
  onChange={(event) => onUpdateTaskStatus(task, event.target.value)}
  disabled={loading}
>
  <option value="requested">requested</option>
  <option value="accepted">accepted</option>
  <option value="in-progress">in-progress</option>
  <option value="on-hold">on-hold</option>
  <option value="completed">completed</option>
  <option value="cancelled">cancelled</option>
</select>

Overdue Task Detection

Task Utilities
const CLOSED_TASK_STATUSES = new Set([
  "completed",
  "cancelled",
  "rejected",
  "failed",
  "entered-in-error"
]);

export const isTaskOpen = (task) => 
  !CLOSED_TASK_STATUSES.has(normalize(task?.status));

export const getTaskDueDate = (task) => {
  return (
    task?.executionPeriod?.end ||
    task?.dueDate ||
    task?.restriction?.period?.end ||
    task?.authoredOn ||
    null
  );
};

export const isTaskOverdue = (task, now = new Date()) => {
  if (!isTaskOpen(task)) {
    return false;
  }

  const dueDate = toDate(getTaskDueDate(task));
  return Boolean(dueDate && dueDate.getTime() < now.getTime());
};

Command Center

The Command Center provides enterprise-level population health monitoring and care coordination.

Dashboard Statistics

Command Center Stats
const dashboardStats = useMemo(() => {
  const now = new Date();
  const next24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
  const openTasks = tasks.filter(isTaskOpen);
  const overdueTasks = openTasks.filter((task) => isTaskOverdue(task, now));
  const highRiskPatients = patientRows.filter(
    (row) => row.profile.tier === "high"
  );
  const totalGaps = patientRows.reduce(
    (sum, row) => sum + row.profile.careGaps.length, 
    0
  );
  const appointmentsNext24h = appointments.filter((appointment) => {
    const start = new Date(appointment.start);
    if (Number.isNaN(start.getTime())) {
      return false;
    }
    return start >= now && start <= next24h;
  });

  return {
    highRiskCount: highRiskPatients.length,
    openTaskCount: openTasks.length,
    overdueTaskCount: overdueTasks.length,
    totalGapCount: totalGaps,
    next24hAppointments: appointmentsNext24h.length,
    noShowRate: calculateNoShowRate(appointments, 90)
  };
}, [appointments, patientRows, tasks]);

Loading Command Center Data

Data Loading
const loadData = async () => {
  const [
    patientBundle,
    conditionBundle,
    allergyBundle,
    medicationBundle,
    observationBundle,
    encounterBundle,
    appointmentBundle,
    taskBundle,
    practitionerResponse
  ] = await Promise.all([
    fhirApi.listPatients(token),
    fhirApi.listConditions(token),
    fhirApi.listAllergies(token),
    fhirApi.listMedicationRequests(token),
    fhirApi.listObservations(token),
    fhirApi.listEncounters(token),
    fhirApi.listAppointments(token),
    fhirApi.listTasks(token),
    adminApi.listPractitioners(token)
  ]);

  setPatients(bundleToResources(patientBundle));
  setConditions(bundleToResources(conditionBundle));
  setAllergies(bundleToResources(allergyBundle));
  setMedications(bundleToResources(medicationBundle));
  setObservations(bundleToResources(observationBundle));
  setEncounters(bundleToResources(encounterBundle));
  setAppointments(bundleToResources(appointmentBundle));
  setTasks(bundleToResources(taskBundle));
  setPractitioners(practitionerResponse.data || []);
};

Care Coordination Worklist

Patient Worklist
const patientRows = useMemo(() => {
  const now = new Date();

  return patients
    .map((patient) => {
      const patientId = patient.id;
      const patientConditions = conditionsByPatient.get(patientId) || [];
      const patientAllergies = allergiesByPatient.get(patientId) || [];
      const patientMedications = medicationsByPatient.get(patientId) || [];
      const patientObservations = observationsByPatient.get(patientId) || [];
      const patientEncounters = encountersByPatient.get(patientId) || [];
      const patientAppointments = appointmentsByPatient.get(patientId) || [];
      const patientTasks = tasksByPatient.get(patientId) || [];

      const profile = buildPatientRiskProfile({
        conditions: patientConditions,
        allergies: patientAllergies,
        medications: patientMedications,
        observations: patientObservations,
        encounters: patientEncounters,
        appointments: patientAppointments,
        tasks: patientTasks
      });

      const openTasks = patientTasks.filter(isTaskOpen);
      const overdueTasks = openTasks.filter((task) => isTaskOverdue(task, now));

      const nextAppointment = [...patientAppointments]
        .filter((appointment) => {
          const start = new Date(appointment.start);
          return !Number.isNaN(start.getTime()) && start >= now;
        })
        .sort((a, b) => sortByDateAsc(a.start, b.start))[0];

      const lastEncounter = [...patientEncounters].sort(
        (a, b) =>
          new Date(b.period?.start || 0).getTime() - 
          new Date(a.period?.start || 0).getTime()
      )[0];

      return {
        patient,
        profile,
        openTaskCount: openTasks.length,
        overdueTaskCount: overdueTasks.length,
        nextAppointment,
        lastEncounter
      };
    })
    .sort((left, right) => {
      // Sort by risk score, then by overdue tasks
      if (right.profile.score !== left.profile.score) {
        return right.profile.score - left.profile.score;
      }
      return right.overdueTaskCount - left.overdueTaskCount;
    });
}, [
  allergiesByPatient,
  appointmentsByPatient,
  conditionsByPatient,
  encountersByPatient,
  medicationsByPatient,
  observationsByPatient,
  patients,
  tasksByPatient
]);

Population Filters

Filtering Options
const filteredPatientRows = useMemo(() => {
  return patientRows.filter((row) => {
    if (riskFilter !== "all" && row.profile.tier !== riskFilter) {
      return false;
    }

    if (ownerFilter === "all") {
      return true;
    }

    const patientTasks = tasksByPatient.get(row.patient.id) || [];
    const openTasks = patientTasks.filter(isTaskOpen);

    if (ownerFilter === "unassigned") {
      return openTasks.some((task) => !ownerReference(task));
    }

    return openTasks.some(
      (task) => ownerReference(task) === `Practitioner/${ownerFilter}`
    );
  });
}, [ownerFilter, patientRows, riskFilter, tasksByPatient]);

// Render filters
<article className="card form-grid two-columns">
  <h2>Population filters</h2>
  
  <label>
    Risk tier
    <select value={riskFilter} onChange={(e) => setRiskFilter(e.target.value)}>
      <option value="all">All tiers</option>
      <option value="high">High</option>
      <option value="medium">Medium</option>
      <option value="low">Low</option>
    </select>
  </label>
  
  <label>
    Task owner
    <select value={ownerFilter} onChange={(e) => setOwnerFilter(e.target.value)}>
      <option value="all">All owners</option>
      <option value="unassigned">Unassigned</option>
      {practitioners.map((practitioner) => (
        <option key={practitioner.id} value={practitioner.id}>
          {practitioner.fullName}
        </option>
      ))}
    </select>
  </label>
</article>

Service Line Demand

Service Mix Analysis
export const calculateServiceMix = (appointments) => {
  const counts = appointments.reduce((map, appointment) => {
    const label =
      appointment?.serviceCategory?.[0]?.text ||
      appointment?.serviceCategory?.[0]?.coding?.[0]?.display ||
      "Unspecified";

    map.set(label, (map.get(label) || 0) + 1);
    return map;
  }, new Map());

  return Array.from(counts.entries())
    .map(([service, count]) => ({ service, count }))
    .sort((a, b) => b.count - a.count);
};

const serviceMix = useMemo(() => {
  const rows = calculateServiceMix(appointments);
  const total = rows.reduce((sum, row) => sum + row.count, 0);

  return rows.slice(0, 8).map((row) => ({
    ...row,
    share: total ? Math.round((row.count / total) * 1000) / 10 : 0
  }));
}, [appointments]);

Risk Stratification

OmniEHR automatically calculates patient risk scores based on clinical data.

Risk Profile Builder

buildPatientRiskProfile()
export const buildPatientRiskProfile = ({
  conditions = [],
  allergies = [],
  medications = [],
  observations = [],
  encounters = [],
  appointments = [],
  tasks = []
}) => {
  const activeConditions = conditions.filter((condition) =>
    ACTIVE_CONDITION_STATUSES.has(statusOfCondition(condition))
  );

  const activeMedications = medications.filter(
    (medication) => ![
      "stopped", "completed", "cancelled", "entered-in-error"
    ].includes(normalize(medication.status))
  );

  const severeAllergies = allergies.filter((allergy) => {
    const reactionSeverity = normalize(allergy?.reaction?.[0]?.severity);
    return normalize(allergy.criticality) === "high" || 
           reactionSeverity === "severe";
  });

  const careGaps = [];
  const safetyAlerts = [];

  // Build care gaps and safety alerts...
  // (See Care Gaps section for full implementation)

  const openTasks = tasks.filter(isTaskOpen);
  const overdueTasks = openTasks.filter((task) => isTaskOverdue(task));

  // Calculate risk score
  let score = 0;
  score += safetyAlerts.filter((alert) => alert.severity === "high").length * 4;
  score += safetyAlerts.filter((alert) => alert.severity === "medium").length * 2;
  score += careGaps.filter((gap) => gap.severity === "high").length * 3;
  score += careGaps.filter((gap) => gap.severity === "medium").length * 2;
  score += careGaps.filter((gap) => gap.severity === "low").length;

  if (overdueTasks.length > 0) {
    score += 2;
  }
  if (openTasks.length >= 5) {
    score += 1;
  }

  return {
    score,
    tier: classifyRiskTier(score),
    careGaps,
    safetyAlerts,
    latestVitals: {
      systolic: systolicValue,
      diastolic: diastolicValue,
      a1c: a1cValue
    },
    activeConditionCount: activeConditions.length,
    activeMedicationCount: activeMedications.length,
    openTaskCount: openTasks.length,
    overdueTaskCount: overdueTasks.length
  };
};

Risk Tier Classification

Risk Tiers
export const classifyRiskTier = (score) => {
  if (score >= 8) {
    return "high";
  }

  if (score >= 4) {
    return "medium";
  }

  return "low";
};
Risk Scoring: High risk (8+), Medium risk (4-7), Low risk (0-3). Scores are calculated from safety alerts, care gaps, and task burden.

Care Gaps Detection

OmniEHR automatically identifies quality improvement opportunities and preventive care needs.

Care Gap Rules

const hasDiabetes = activeConditions.some((condition) => {
  const code = normalize(codingOf(condition).code);
  const label = textOf(condition);
  return code === "44054006" || label.includes("diabetes");
});

if (hasDiabetes && !hasRecentObservation(observations, A1C_CODES, 180)) {
  careGaps.push({
    severity: "high",
    title: "HbA1c follow-up overdue",
    detail: "No HbA1c result found in the last 180 days for an active diabetes condition."
  });
}
if ((hasHypertension || 
     (systolicValue || 0) >= 140 || 
     (diastolicValue || 0) >= 90) && 
    !hasRecentObservation(observations, SYSTOLIC_BP_CODES, 30)) {
  careGaps.push({
    severity: "medium",
    title: "Blood pressure follow-up needed",
    detail: "No recent blood-pressure observation found in the last 30 days."
  });
}
const latestEncounter = latestByDate(
  encounters, 
  (encounter) => encounter.period?.start
);

if (!latestEncounter || 
    daysBetween(toDate(latestEncounter.period?.start)) > 180) {
  careGaps.push({
    severity: "medium",
    title: "Continuity-of-care visit due",
    detail: "No encounter documented in the last 180 days."
  });
}
const hasMedicationReviewVisit = appointments.some((appointment) => {
  const description = appointmentDescription(appointment);
  const start = toDate(appointment.start);
  return (
    Boolean(start) &&
    daysBetween(start) <= 120 &&
    (description.includes("medication review") || 
     description.includes("follow-up"))
  );
});

if (activeMedications.length >= 5 && !hasMedicationReviewVisit) {
  careGaps.push({
    severity: "medium",
    title: "Medication reconciliation due",
    detail: "Polypharmacy profile without a recent medication-review follow-up."
  });
}
if (!hasUpcomingAppointmentWithin(appointments, 60)) {
  careGaps.push({
    severity: "low",
    title: "No upcoming follow-up appointment",
    detail: "No booked appointment found in the next 60 days."
  });
}

Safety Alerts

Clinical safety alerts are generated based on real-time patient data.

Safety Alert Rules

if (severeAllergies.length > 0) {
  safetyAlerts.push({
    severity: "high",
    title: "Severe allergy profile",
    detail: `${severeAllergies.length} severe/high-criticality allergy record(s) require active review.`
  });
}
const medicationConflicts = activeMedications.filter((medication) => {
  const medicationLabel = normalize(
    medication?.medicationCodeableConcept?.coding?.[0]?.display ||
    medication?.medicationCodeableConcept?.coding?.[0]?.code
  );

  if (!medicationLabel) {
    return false;
  }

  return allergies.some((allergy) => {
    if (!normalize((allergy.category || []).join(" "))
        .includes("medication")) {
      return false;
    }

    const allergyLabel = textOf(allergy);
    if (!allergyLabel) {
      return false;
    }

    return medicationLabel.includes(allergyLabel) || 
           allergyLabel.includes(medicationLabel);
  });
});

if (medicationConflicts.length > 0) {
  safetyAlerts.push({
    severity: "high",
    title: "Potential allergy-medication conflict",
    detail: `${medicationConflicts.length} active medication(s) may conflict with allergy records.`
  });
}
if ((systolicValue || 0) >= 180 || (diastolicValue || 0) >= 120) {
  safetyAlerts.push({
    severity: "high",
    title: "Hypertensive crisis threshold",
    detail: `Latest BP ${systolicValue || "-"} / ${diastolicValue || "-"} mmHg is above crisis threshold.`
  });
}

Clinical Decision Support

Patient detail pages display real-time clinical decision support:
Clinical Decision Support Display
<article className="card form-grid two-columns">
  <h2>Clinical decision support</h2>
  
  <div className="metric-card">
    <h3>Risk tier</h3>
    <p className="metric-value">
      <span className={`risk-chip risk-chip-${riskProfile.tier}`}>
        {riskProfile.tier} ({riskProfile.score})
      </span>
    </p>
    <p className="muted-text">
      {riskProfile.openTaskCount} open tasks, 
      {riskProfile.overdueTaskCount} overdue.
    </p>
  </div>
  
  <div className="metric-card">
    <h3>Latest vitals</h3>
    <p className="muted-text">
      BP: {riskProfile.latestVitals.systolic || "-"} / 
      {riskProfile.latestVitals.diastolic || "-"} mmHg
    </p>
    <p className="muted-text">
      HbA1c: {riskProfile.latestVitals.a1c || "-"}
    </p>
  </div>
  
  <div>
    <h3>Safety alerts</h3>
    {riskProfile.safetyAlerts.length === 0 ? (
      <p className="muted-text">No active alerts.</p>
    ) : (
      <ul className="plain-list">
        {riskProfile.safetyAlerts.map((alert) => (
          <li key={`${alert.title}-${alert.detail}`}>
            <span className={`risk-chip risk-chip-${alert.severity}`}>
              {alert.severity}
            </span>
            {" "}
            {alert.title}
          </li>
        ))}
      </ul>
    )}
  </div>
  
  <div>
    <h3>Open care gaps</h3>
    {riskProfile.careGaps.length === 0 ? (
      <p className="muted-text">No open care gaps.</p>
    ) : (
      <ul className="plain-list">
        {riskProfile.careGaps.map((gap) => (
          <li key={`${gap.title}-${gap.detail}`}>
            <span className={`risk-chip risk-chip-${gap.severity}`}>
              {gap.severity}
            </span>
            {" "}
            {gap.title}
          </li>
        ))}
      </ul>
    )}
  </div>
</article>

Task Inbox

The Command Center includes a comprehensive task inbox with overdue detection:
Task Inbox
const taskRows = useMemo(() => {
  return [...tasks].sort((left, right) => {
    const overdueLeft = isTaskOverdue(left) ? 1 : 0;
    const overdueRight = isTaskOverdue(right) ? 1 : 0;
    if (overdueLeft !== overdueRight) {
      return overdueRight - overdueLeft;
    }

    return sortByDateAsc(getTaskDueDate(left), getTaskDueDate(right));
  });
}, [tasks]);

// Render task table
<table>
  <thead>
    <tr>
      <th>Task</th>
      <th>Patient</th>
      <th>Owner</th>
      <th>Priority</th>
      <th>Due</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    {taskRows.map((task) => {
      const dueDate = getTaskDueDate(task);
      const isOverdue = isTaskOverdue(task);

      return (
        <tr key={task.id}>
          <td>
            <p>{task.description || "-"}</p>
            {task.code?.text ? (
              <p className="muted-text">{task.code.text}</p>
            ) : null}
          </td>
          <td>
            {patient ? (
              <Link to={`/patients/${patient.id}`} className="inline-link">
                {patientFullName(patient)}
              </Link>
            ) : "-"}
          </td>
          <td>{owner}</td>
          <td>
            <span className={`priority-chip priority-chip-${task.priority}`}>
              {task.priority || "routine"}
            </span>
          </td>
          <td>
            {formatDateTime(dueDate)}
            {isOverdue ? (
              <p className="status-text-overdue">Overdue</p>
            ) : null}
          </td>
          <td>
            <select
              value={task.status}
              onChange={(event) => onUpdateTaskStatus(task, event.target.value)}
            >
              {TASK_STATUS_OPTIONS.map((status) => (
                <option key={status} value={status}>
                  {status}
                </option>
              ))}
            </select>
          </td>
        </tr>
      );
    })}
  </tbody>
</table>

Role-Based Task Access

Practitioners can only access tasks assigned to them:
Task Owner Permission
const ensureTaskOwnerPermission = (requestingUser, ownerUserId) => {
  if (requestingUser.role !== "practitioner") {
    return;
  }

  if (!ownerUserId || String(ownerUserId) !== String(requestingUser.sub)) {
    throw new ApiError(403, "Practitioners can only assign or update tasks under their own worklist");
  }
};

// Backend query filtering
router.get(
  "/Task",
  authorize(...readRoles),
  asyncHandler(async (req, res) => {
    const filter = {};

    if (req.user.role === "practitioner") {
      filter.ownerUserId = req.user.sub;
    }

    const records = await Task.find(filter)
      .sort({ dueDate: 1, authoredOn: -1 })
      .limit(300);
    
    const resources = records.map(taskDocToResource);

    res.json(
      toSearchsetBundle({
        resourceType: "Task",
        resources,
        total: resources.length,
        baseUrl: baseUrl(req),
        searchId: randomUUID()
      })
    );
  })
);

API Reference

Task Endpoints

MethodEndpointDescriptionRequired Role
POST/api/fhir/TaskCreate taskadmin, practitioner
GET/api/fhir/TaskList tasksadmin, practitioner, auditor
GET/api/fhir/Task/:idGet task by IDadmin, practitioner, auditor
PUT/api/fhir/Task/:idUpdate taskadmin, practitioner

Query Parameters

GET /api/fhir/Task?for=Patient/507f1f77bcf86cd799439011

Next Steps

Patient Management

Learn about patient registry and demographics

Scheduling

Explore appointment scheduling system

FHIR Resources

View all supported FHIR resources

Audit Logs

Learn about HIPAA-compliant audit logging

Build docs developers (and LLMs) love