@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio&#038;p=147129>
  a sioc:Post ;
  dc:title "Database schema" ;
  dcterms:identifier 147129 ;
  dc:modified "2026-09-11T16:42:20Z"^^xsd:dateTime ;
  dc:created "2026-09-07T19:13:22Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-tasks/database-schema/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Type:</strong> Doc-section</li><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-tasks/">LH Tasks</a></li></ul><p>One custom table, per-site (not centralised — the atomic claim UPDATE joins against the posts table, and post IDs are only unique within a single site&#8217;s own posts table on Multisite): <code>{$wpdb-&gt;prefix}lh_tasks_status</code>. Everything else lives in WordPress&#8217;s own posts, postmeta, terms/term_relationships/term_taxonomy tables (or their per-site equivalents on Multisite).</p>
<h2>Post type</h2>
<p><code>lh_tasks-task_post</code></p>
<h2>Post status (native WordPress)</h2>
<p><code>draft</code> / <code>pending</code> / <code>lh_tasks-active</code> / <code>private</code> / <code>future</code> / <code>trash</code>. <code>lh_tasks-active</code> (added 1.6.0, replaces <code>publish</code> for this CPT) is required for a task to be claimable via <code>claim_next_task()</code>, and is registered <code>public =&gt; false</code> — front-end read access is gated per-task via <code>map_meta_cap</code>, not blanket-public. <code>create_task()</code> defaults to <code>lh_tasks-active</code>; pass <code>draft</code> or <code>pending</code> for a task that shouldn&#8217;t be visible/claimable until reviewed.</p>
<h2>Workflow status (<code>{$wpdb-&gt;prefix}lh_tasks_status</code> table)</h2>
<table>
<thead>
<tr>
<th>Status key (namespace <code>lh_tasks-</code>)</th>
<th>Label</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>new_task</code></td>
<td>New</td>
<td>Initial state; claimable (subject to post_status = <code>lh_tasks-active</code>)</td>
</tr>
<tr>
<td><code>claim_task</code></td>
<td>Claimed</td>
<td>Set atomically by <code>claim_next_task()</code>; <code>renew_task()</code> (the <code>renew-claim</code> heartbeat) keeps a task in this status while pushing <code>claimed_at</code> forward</td>
</tr>
<tr>
<td><code>prog_task</code></td>
<td>In Progress</td>
<td>For workers that separate &#8220;claimed&#8221; from &#8220;started&#8221;; not set automatically</td>
</tr>
<tr>
<td><code>pend_task</code></td>
<td>Pending</td>
<td>Manual/human-workflow use</td>
</tr>
<tr>
<td><code>block_task</code></td>
<td>Blocked</td>
<td>Set automatically when retries are exhausted, or when a blocked dependency is encountered at claim time</td>
</tr>
<tr>
<td><code>res_task</code></td>
<td>Resolved</td>
<td>Set by <code>complete_task()</code>; terminal</td>
</tr>
<tr>
<td><code>clos_task</code></td>
<td>Closed</td>
<td>Manual final closure after resolution; terminal</td>
</tr>
<tr>
<td><code>cncl_task</code></td>
<td>Cancelled</td>
<td>Terminal</td>
</tr>
<tr>
<td><code>defr_task</code></td>
<td>Deferred</td>
<td>Parked for later — deliberately worth doing eventually, not urgent now. Not in <code>return_open_statuses()</code> (won&#8217;t dilute the dashboard widget/default list) and not terminal (promotable back to New later via <code>reassign_task()</code>)</td>
</tr>
</tbody>
</table>
<p>Status key naming (e.g. <code>pend_task</code> not <code>pending_task</code>) is unchanged from when these were post_status values constrained to 20 characters — the table column isn&#8217;t constrained the same way, but there was no reason to rename and break continuity with existing data/consumers.</p>
<p>Table columns (as of 1.6.0, table version 2.0): <code>post_id</code> (PK), <code>status</code>, <code>claim_token</code>, <code>assigned_to</code> (BIGINT UNSIGNED NULL — hard requirement), <code>assignee</code> (BIGINT UNSIGNED NULL — current claim holder), <code>claimed_at</code>, <code>retry_count</code>, <code>max_retries</code>, <code>created_at</code>, <code>updated_at</code>.</p>
<h2>Taxonomies</h2>
<ul>
<li><code>lh_tasks_queue</code> — non-hierarchical, non-public. Routing/lane, e.g. <code>email-triage</code>. Admin column shown on the Tasks list screen.</li>
<li><code>lh_tasks_task_type</code> — non-hierarchical, non-public. Classification of the kind of work, e.g. <code>github_triage</code>. Distinct from queue: a queue can carry more than one task type and vice versa. Admin column shown on the Tasks list screen.</li>
</ul>
<h2>Post meta (all keys prefixed <code>_lh_tasks-</code>)</h2>
<table>
<thead>
<tr>
<th>Meta key</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>payload</code></td>
<td>JSON string</td>
<td>Machine-actionable spec for agent tasks, e.g. <code>{"ability": "lh-email/send-broadcast", "params": {...}}</code></td>
</tr>
<tr>
<td><code>result</code></td>
<td>JSON string</td>
<td>Output on completion, or <code>{"error": "..."}</code> on failure</td>
</tr>
<tr>
<td><code>priority</code></td>
<td>string</td>
<td><code>low</code>|<code>normal</code>|<code>high</code>|<code>urgent</code></td>
</tr>
<tr>
<td><code>due_date</code></td>
<td>MySQL datetime</td>
<td>Optional</td>
</tr>
<tr>
<td><code>blocked_by</code></td>
<td>array of int</td>
<td>Task IDs that must reach a terminal status first</td>
</tr>
<tr>
<td><code>source_plugin</code></td>
<td>string</td>
<td>Slug of the plugin that created this task, e.g. <code>lh-crm</code></td>
</tr>
<tr>
<td><code>source_post_id</code></td>
<td>int</td>
<td>ID of the originating post in the source plugin</td>
</tr>
<tr>
<td><code>progress_log</code></td>
<td>array of objects</td>
<td>Appended to by <code>reassign_task()</code>, <code>renew_task()</code> (only when a <code>note</code> is given), and <code>edit_task()</code>. Each entry: <code>timestamp</code>, <code>event</code> (<code>reassigned</code>, <code>heartbeat</code>, or <code>admin_override</code>), <code>from_status</code>/<code>to_status</code> (reassign/admin_override only), <code>from_assignee</code>/<code>to_assigned_to</code> (reassign only), <code>actor</code> (WP user ID), <code>note</code></td>
</tr>
</tbody>
</table>
<p><code>assignee</code>, <code>assigned_to</code>, <code>claim_token</code>, <code>claimed_at</code>, <code>retry_count</code>, and <code>max_retries</code> live in the status table, not postmeta — see above.</p>
"""^^rdf:XMLLiteral ;
  sioc:content """Type: Doc-sectionPart of: LH TasksOne custom table, per-site (not centralised — the atomic claim UPDATE joins against the posts table, and post IDs are only unique within a single site&#8217;s own posts table on Multisite): {$wpdb-&gt;prefix}lh_tasks_status. Everything else lives in WordPress&#8217;s own posts, postmeta, terms/term_relationships/term_taxonomy tables (or their per-site equivalents on Multisite).
Post type
lh_tasks-task_post
Post status (native WordPress)
draft / pending / lh_tasks-active / private / future / trash. lh_tasks-active (added 1.6.0, replaces publish for this CPT) is required for a task to be claimable via claim_next_task(), and is registered public =&gt; false — front-end read access is gated per-task via map_meta_cap, not blanket-public. create_task() defaults to lh_tasks-active; pass draft or pending for a task that shouldn&#8217;t be visible/claimable until reviewed.
Workflow status ({$wpdb-&gt;prefix}lh_tasks_status table)



Status key (namespace lh_tasks-)
Label
Notes




new_task
New
Initial state; claimable (subject to post_status = lh_tasks-active)


claim_task
Claimed
Set atomically by claim_next_task(); renew_task() (the renew-claim heartbeat) keeps a task in this status while pushing claimed_at forward


prog_task
In Progress
For workers that separate &#8220;claimed&#8221; from &#8220;started&#8221;; not set automatically


pend_task
Pending
Manual/human-workflow use


block_task
Blocked
Set automatically when retries are exhausted, or when a blocked dependency is encountered at claim time


res_task
Resolved
Set by complete_task(); terminal


clos_task
Closed
Manual final closure after resolution; terminal


cncl_task
Cancelled
Terminal


defr_task
Deferred
Parked for later — deliberately worth doing eventually, not urgent now. Not in return_open_statuses() (won&#8217;t dilute the dashboard widget/default list) and not terminal (promotable back to New later via reassign_task())



Status key naming (e.g. pend_task not pending_task) is unchanged from when these were post_status values constrained to 20 characters — the table column isn&#8217;t constrained the same way, but there was no reason to rename and break continuity with existing data/consumers.
Table columns (as of 1.6.0, table version 2.0): post_id (PK), status, claim_token, assigned_to (BIGINT UNSIGNED NULL — hard requirement), assignee (BIGINT UNSIGNED NULL — current claim holder), claimed_at, retry_count, max_retries, created_at, updated_at.
Taxonomies

lh_tasks_queue — non-hierarchical, non-public. Routing/lane, e.g. email-triage. Admin column shown on the Tasks list screen.
lh_tasks_task_type — non-hierarchical, non-public. Classification of the kind of work, e.g. github_triage. Distinct from queue: a queue can carry more than one task type and vice versa. Admin column shown on the Tasks list screen.

Post meta (all keys prefixed _lh_tasks-)



Meta key
Type
Description




payload
JSON string
Machine-actionable spec for agent tasks, e.g. {"ability": "lh-email/send-broadcast", "params": {...}}


result
JSON string
Output on completion, or {"error": "..."} on failure


priority
string
low|normal|high|urgent


due_date
MySQL datetime
Optional


blocked_by
array of int
Task IDs that must reach a terminal status first


source_plugin
string
Slug of the plugin that created this task, e.g. lh-crm


source_post_id
int
ID of the originating post in the source plugin


progress_log
array of objects
Appended to by reassign_task(), renew_task() (only when a note is given), and edit_task(). Each entry: timestamp, event (reassigned, heartbeat, or admin_override), from_status/to_status (reassign/admin_override only), from_assignee/to_assigned_to (reassign only), actor (WP user ID), note



assignee, assigned_to, claim_token, claimed_at, retry_count, and max_retries live in the status table, not postmeta — see above.
""" ;
  sioc:topic <https://lhero.org/lh_portfolio-type/doc-section/>, <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/lh_portfolio-type/doc-section/> rdfs:seeAlso <https://lhero.org/lh_portfolio-type/doc-section/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
