CivicLoop by Ta-Tech Solutions Purpose: Define every entity in the system, how they relate, and the lifecycle a service request moves through. This is the contract the database, the API, and every screen are built against.
CivicLoop has one central object: the Service Request. Everything else either describes it, routes it, works it, or communicates about it. If you understand the Service Request and its lifecycle, you understand the system.
A Service Request is a living record. It is created once, by a resident or an agent, and from that moment it carries its own status, its own owner, its own clock, its own history, and its own conversation - visible to the resident, the assigned agent, and County leadership at the same time. It is the opposite of the "black hole."
| Entity | What it is | Key fields |
|---|---|---|
| County | The top-level tenant. CivicLoop is multi-tenant; Prince George's County is one tenant. The model supports many. | id, name, state, timezone, default_language, branding (logo, colors), Open311 endpoint config, autopilot_level (text: off / route / full; controls how much the AI does on its own at intake - Document 07, Component J) |
| Department | An operating unit the County routes work to: DPWT, DOE, WSSC, DPIE, PARKS, ANIMAL, 311 (Office of Community Relations), Health & Human Services, etc. Each department also owns one persistent channel (Section 2.7 below). | id, county_id, name, code, contact, business_hours, escalation_contact |
| Service Area | A geographic subdivision used for routing and analytics: council district, planning area, sub-division, or a custom polygon. | id, county_id, name, type, boundary (geo polygon), council_district (1-9 for PG County, used by the equity panel and the council view), zip_code |
| Location | A point on the map attached to a request. Captured from device GPS, a dropped pin, or a geocoded address. | lat, lng, address, service_area_id, accuracy_m, source (gps / pin / geocoded), council_district, zip_code (both denormalized from the resolved service area so the equity, forecast, and council-view queries do not have to join a geometry on every read) |
| Entity | What it is | Key fields |
|---|---|---|
| Service Category | The kind of problem. A two-level tree: a top group (Roads & Sidewalks, Trash & Recycling, Streetlights, Noise, Parks, Housing, Animal Services, Health & Human Services, etc.) and specific types under it (pothole, missed collection, broken streetlight...). Each category carries its default routing and SLA. | id, county_id, parent_id, name, default_department_id, default_sla_hours, default_priority, open311_service_code, requires_location, requires_photo |
| Service Request | The central living record. | id, county_id, request_number, category_id, department_id, status, priority, location_id, description, channel, language, resident_id, created_by, assigned_to, sla_due_at, sla_predicted_breach, sentiment, duplicate_of_id, opened_at, acknowledged_at, resolved_at, closed_at |
| Request Attachment | A photo, video, voice clip, or document on the request - at intake (resident's evidence) or at resolution (proof of work). | id, request_id, type, url, caption, stage (intake / progress / resolution), uploaded_by |
| Request Event | An append-only history entry. Every status change, assignment, comment, notification, and AI action writes one. The request's full timeline. | id, request_id, event_type, actor (resident / agent / ai / system), from_value, to_value, note, created_at |
| Request Comment | Two-way message thread between resident and agent on a request. | id, request_id, author, body, internal (true = staff-only note), created_at |
| Entity | What it is | Key fields |
|---|---|---|
| Resident | A member of the public. Can exist with just a phone number - no account required to file a request. An optional account adds tracking convenience. | id, county_id, name (optional), phone, email (optional), preferred_language, preferred_channel, has_account, created_at |
| Staff | A County employee who works inside CivicLoop: agent, supervisor, department head, 311 director, county admin. | id, county_id, department_id, name, email, role, active, mfa_enrolled, last_seen_at |
(The full role and permission model is Document 04.)
| Entity | What it is | Key fields |
|---|---|---|
| Routing Rule | How a category (optionally narrowed by service area or keyword) maps to a department and priority. The AI proposes routing; rules are the deterministic backbone it works within and can be overridden by. | id, county_id, category_id, service_area_id (optional), keyword_match (optional), department_id, priority, active |
| SLA Policy | The promised resolution time for a category + priority, in business hours, against a department's business calendar. | id, county_id, category_id, priority, response_hours, resolution_hours, business_hours_only |
| AI Decision | A record of every AI action on a request - what it did, what it concluded, its confidence, and the rationale shown to humans. Auditable; the AI is never a black box. | id, request_id, decision_type (classify / route / predict_breach / detect_sentiment / detect_duplicate), input_summary, output, confidence, rationale, model_version, created_at |
| Entity | What it is | Key fields |
|---|---|---|
| Notification | An outbound message to a resident about their request - received, assigned, in progress, resolved, needs-more-info. Carries language and channel. | id, request_id, resident_id, channel (sms / whatsapp / email / push), language, template, body, status (queued / sent / delivered / failed), sent_at |
| Notification Template | The localized message templates, one set per language, for each lifecycle event. | id, county_id, event_type, language, subject, body |
| Entity | What it is | Key fields |
|---|---|---|
| Audit Log | Every meaningful action by any staff member or system process, immutable, attributable. The compliance spine (Document 09). | id, county_id, actor, action, entity_type, entity_id, before, after, ip, created_at |
| Saved View | A staff member's filtered queue (e.g. "my open high-priority road requests"). | id, staff_id, name, filter_json |
These five tables landed in web/sql/civicpulse-schema.sql as part
of the post-core build wave. Each has row-level security on
county_id and each ships with INSERT/DELETE smoke tests in the
schema dump (TaTech "trigger smoke test rule").
| Entity | What it is | Key fields |
|---|---|---|
Scheduled Visit (scheduled_visits) |
A field-crew or follow-up visit an agent puts on a resident's request. Creating one fires SMS + email to the resident with a .ics calendar attachment; crews can set a 24h alarm; cancel + complete actions both write Request Events on the request. |
id, county_id, request_id, scheduled_at (timestamptz), duration_minutes, alarm_minutes_before, address (snapshot), notes, created_by, status (scheduled / completed / cancelled), completed_at |
Channel (channels) |
A persistent Slack-style channel. One per County department (DPWT, DOE, WSSC, DPIE, PARKS, ANIMAL, 311) plus a county-wide #311-all. Drives /channels and /channels/[slug]. |
id, county_id, slug, name, department_id (nullable for #311-all), kind (department / county), created_at |
Channel Message (channel_messages) |
One message in a channel. Supports @mention highlighting, full-text substring search, and auto-linked CP-... tracking numbers that deep-link into /console/[requestNumber]. Slash commands (/help, /open, /breaches, /summary CP-...) are processed deterministically server-side, no AI call. @loop mentions trigger the Loop persona (Document 07). |
id, county_id, channel_id, author_id (nullable when author is loop system), body, mentions[], request_refs[] (parsed CP-... numbers), created_at |
Request Survey (request_surveys) |
Created on every transition to RESOLVED. A one-tap 1-5 link is texted/emailed to the resident; the public survey page lives at /[locale]/survey/[token]. Responses feed the NPS panel on the director dashboard. |
id, county_id, request_id, token (opaque, single-use), score (1-5, nullable until answered), comment, sent_at, responded_at |
Predicted Issue (predicted_issues) |
One forecast row per (category, council_district, run_id): expected request count for the next 7 days, a confidence score, the run timestamp. Populated by the forecast component (Document 07, Component K); read by the dashboard forecast panel and by the council-district view. |
id, county_id, category_id, council_district, expected_count, confidence, horizon_days (7), computed_at |
County
|-- Department (many)
|-- Service Area (many)
|-- Service Category (many, 2-level tree)
|-- Staff (many)
|-- Resident (many)
|-- Routing Rule (many)
|-- SLA Policy (many)
|-- Notification Template (many)
|
+-- Service Request (many) <-- the center of everything
|-- belongs to one Service Category
|-- routed to one Department
|-- has one Location -> one Service Area
|-- filed by / about one Resident
|-- assigned to one Staff (agent)
|-- has many Request Attachments
|-- has many Request Events (the timeline)
|-- has many Request Comments (the conversation)
|-- has many AI Decisions (every AI action, auditable)
|-- has many Notifications (every message to the resident)
|-- may reference another Request as duplicate_of
Every box above is tenant-scoped to a County. A second county added to CivicLoop gets its own departments, categories, rules, staff, and requests, fully isolated - the multi-tenant property inherited from the Ta-Tech engine (Document 05).
The 2.7 wave plugs in cleanly:
Service Request ---- Scheduled Visit (0..n; ICS + alarm + SMS/email)
---- Request Survey (0..1; one per RESOLVED transition)
Department ---- Channel (1; the dept's slack-style room)
County ---- Channel (1 county-wide: #311-all)
Channel ---- Channel Message (0..n; including @loop replies)
Channel Message ----> Service Request (0..n; auto-parsed CP-... refs)
(Category, Service Area) ---- Predicted Issue (0..n; one per forecast run)
Triggers worth knowing about:
requests_after_update_to_resolved writes the request_surveys
row (one survey per RESOLVED transition; never duplicates if the
status flips back and forth).scheduled_visits_after_insert writes a Request Event with the
actor of the agent who scheduled it; the notifications fire from
the visit lib (web/src/lib/visits/*) on the same transaction.This is the state machine. Every transition writes a Request Event and may fire a Notification.
+-------------+
| DRAFT | resident is still composing (intake AI
+-----+-------+ conversation in progress); not yet visible
| to staff
v
+-------------+
| SUBMITTED | resident confirmed; request_number issued;
+-----+-------+ AI classifies + routes; resident gets
| "received" notification
v
+-------------+
| TRIAGED | routed to a Department with a priority and
+-----+-------+ an SLA clock; AI duplicate-check run; if a
| duplicate, link + merge, notify resident
v
+-------------+
| ASSIGNED | a specific agent owns it; resident gets
+-----+-------+ "a crew/agent is assigned" notification
|
v
+-------------+
| IN PROGRESS | agent is actively working it; progress
+-----+-------+ updates + photos may be added; SLA-breach
| prediction runs continuously
|
+----------+----------+
v v
+-------------+ +-------------+
| RESOLVED | | NEEDS INFO | agent needs something from the
+-----+-------+ +-----+-------+ resident; clock pauses; resident
| | notified with a question
| +----------> back to IN PROGRESS on reply
v
+-------------+
| CLOSED | resolution confirmed; proof-of-resolution attachment
+-----+-------+ required; resident gets "resolved" notification with
| before/after; resident may reopen within a window
v
+-------------+
| REOPENED | resident says it is not actually fixed; returns to
+-------------+ IN PROGRESS, flagged, supervisor notified
Side states reachable from most points:
These are invariants - the system guarantees them, so no screen or workflow can violate them.
To keep v1 focused and demo-ready for May 23, the model does not yet include (but is structured to accept in Phase 2):
Case entity can group them without a
schema rewrite.asset_id.The discipline: model the v1 loop completely and correctly, leave clean seams for Phase 2, build nothing speculative.
Next: 04 - User Roles & Permissions.