@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-10-23T20:18:14Z"^^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>Part of:</strong> <a href=\"https://lhero.org/portfolio/lh-tasks/\">LH Tasks</a></li></ul><p class=\"wp-block-paragraph\">Two custom tables, per-site (not centralised — the atomic claim UPDATE joins against the posts table, and post IDs are only unique within a single site&#039;s own posts table on Multisite): <code>{$wpdb-&gt;prefix}lh_tasks_status</code> and, as of 1.8.0, <code>{$wpdb-&gt;prefix}lh_tasks_dependencies</code>. Everything else lives in WordPress&#039;s own posts, postmeta, terms/term_relationships/term_taxonomy tables (or their per-site equivalents on Multisite).</p><h2 class=\"wp-block-heading\">Post type</h2><p class=\"wp-block-paragraph\"><code>lh_tasks-task_post</code></p><h2 class=\"wp-block-heading\">Post status (native WordPress)</h2><p class=\"wp-block-paragraph\"><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&#039;t be visible/claimable until reviewed.</p><h2 class=\"wp-block-heading\">Workflow status (<code>{$wpdb-&gt;prefix}lh_tasks_status</code> table)</h2><figure class=\"wp-block-table blocks-engine-table-2364981fefe1-3\"><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 &quot;claimed&quot; from &quot;started&quot;; not set automatically</td></tr><tr><td><code>pend_task</code></td><td>Pending</td><td>Manual/human-workflow use. Also set automatically (1.8.0) when a task waiting in New loses a <code>blocked_by</code> prerequisite to Cancelled, the trash or deletion</td></tr><tr><td><code>revw_task</code></td><td>Awaiting review</td><td>Open (1.23.0, #150480). Set when someone other than the task&#039;s author marks an assigned task done. The author or staff then accepts it (Resolved) or sends it back with a note (New, still assigned to the doer). Never claimable, and mark as done is refused while a task is in it.</td></tr><tr><td><code>fail_task</code></td><td>Failed</td><td>Set automatically when retries are exhausted, with the claim released (1.18.0; was <code>block_task</code>, &quot;Blocked&quot;, renamed so it is not confused with a task held back by a <code>blocked_by</code> prerequisite, which stays New and is skipped by claims). Open, for a person to triage; <code>edit-task</code> with status New and <code>reset_retries</code> requeues it with its full retries</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&#039;t dilute the dashboard widget/default list) and not terminal (promotable back to New later via <code>reassign_task()</code>)</td></tr></tbody></table></figure><p class=\"wp-block-paragraph\">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&#039;t constrained the same way, but there was no reason to rename and break continuity with existing data/consumers. The one exception is <code>block_task</code>, renamed to <code>fail_task</code> in 1.18.0 because the old name was actively misleading.</p><p class=\"wp-block-paragraph\">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><p class=\"wp-block-paragraph\">Table version 2.1 (1.18.0) changes data, not columns: <code>migrate_2_0_to_2_1()</code>, run once per site from <code>maybe_create_table()</code> on init, moves <code>block_task</code> rows to <code>fail_task</code>, and repairs two keys that <code>edit-task</code> stored before 1.17.1 validated statuses: <code>can_task</code> to <code>cncl_task</code> and <code>resolved</code> to <code>res_task</code>. These are plain row updates, so no progress-log entries are written and no status actions fire. The version is in the <code>lh_tasks_status_table_version</code> option.</p><h2 class=\"wp-block-heading\">Dependencies (<code>{$wpdb-&gt;prefix}lh_tasks_dependencies</code> table, 1.8.0)</h2><p class=\"wp-block-paragraph\">One row per &quot;task waits on blocker&quot; edge, and the only store of <code>blocked_by</code> (it replaced the <code>_lh_tasks-blocked_by</code> post meta, which was migrated and deleted). Columns: <code>task_id</code>, <code>blocker_id</code> (primary key together), <code>created_at</code>; indexed on <code>blocker_id</code>. Version option <code>lh_tasks_dependencies_schema_version</code>. Rows for a permanently deleted task are removed in either column.</p><h2 class=\"wp-block-heading\">Taxonomies</h2><ul class=\"wp-block-list\"><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><li><code>lh_tasks_priority</code> (1.15.0) — a locked set of four terms (low, normal, high, urgent), ranked by term meta <code>order</code> (10, 20, 30, 40). Each task carries one. Version option <code>lh_tasks_priority_schema_version</code>. See Architecture notes.</li></ul><h2 class=\"wp-block-heading\">Post meta (all keys prefixed <code>_lh_tasks-</code>)</h2><figure class=\"wp-block-table blocks-engine-table-2364981fefe1-4\"><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: the ability to run and its params</td></tr><tr><td><code>result</code></td><td>JSON string</td><td>Output on completion (since 1.23.0 the completion note, for a task marked done by a person), or an error object on a retryable failure. A stale error is cleared when the task is later completed</td></tr><tr><td><code>due_date</code></td><td>MySQL datetime</td><td>Optional</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>skip_review</code></td><td>int (1)</td><td>Set when the task was created with <code>requires_review</code> false: the assignee&#039;s done resolves it without a review step. 1.23.0</td></tr><tr><td><code>progress_log</code></td><td>array of objects</td><td>Appended to by <code>reassign_task()</code>, <code>update_task()</code>, <code>add_note()</code>, <code>complete_assigned_task()</code>, <code>review_task()</code>, <code>LH_Tasks_dependencies_class::flag_dependents()</code>, <code>LH_Tasks_follow_up_class</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>, <code>admin_override</code>, <code>prerequisite_dropped</code>, <code>note</code> (1.10.0), <code>marked_done</code> (1.11.0, when the assigned member marks it done from their profile, with their note if they gave one), <code>follow_up</code> and <code>follow_up_escalated</code> (1.16.0, written by the daily follow-up with actor 0, and never counted as activity), <code>submitted_for_review</code>, <code>review_accepted</code> and <code>review_sent_back</code> (1.23.0, the review step, each with <code>to_status</code>), or <code>updated</code>, which also records <code>fields</code> and, if the task was claimed, <code>claimed_by</code>), <code>from_status</code>/<code>to_status</code> (reassigned, admin_override and prerequisite_dropped only), <code>from_assignee</code>/<code>to_assigned_to</code> (reassign only), <code>retries_reset_from</code> (admin_override with <code>reset_retries</code>, 1.18.0: the retry count before it was reset), <code>via</code> (1.23.0: <code>mcp</code> when a completion or review came through an ability rather than the task page), <code>actor</code> (WP user ID), <code>note</code></td></tr><tr><td><code>follow_up</code></td><td>array</td><td>Where an assigned task is in the follow-up cycle, including when the last follow-up was sent (<code>at</code>); activity after that time starts the cycle again. 1.16.0</td></tr><tr><td><code>claim_started</code></td><td>Unix timestamp</td><td>When the current claim was made, set on <code>lh_tasks_task_claimed</code>; the start of the maximum lease for automatic renewal. 1.17.0</td></tr><tr><td><code>ability_calls</code></td><td>int</td><td>Count of write ability calls made with this task (succeeded or failed, not refused). 1.17.0</td></tr><tr><td><code>last_ability</code></td><td>array</td><td>The last such call: <code>ability</code>, <code>time_gmt</code>, <code>success</code>, <code>renewed</code>. 1.17.0</td></tr></tbody></table></figure><p class=\"wp-block-paragraph\"><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. The follow-up&#039;s BuddyPress notifications carry two notification meta keys, <code>lh_tasks_blog_id</code> and <code>lh_tasks_link</code>, recording the task&#039;s site and link (1.16.0).</p>"^^rdf:XMLLiteral ;
  sioc:content "Part of: LH TasksTwo custom tables, per-site (not centralised — the atomic claim UPDATE joins against the posts table, and post IDs are only unique within a single site&#039;s own posts table on Multisite): {$wpdb-&gt;prefix}lh_tasks_status and, as of 1.8.0, {$wpdb-&gt;prefix}lh_tasks_dependencies. Everything else lives in WordPress&#039;s own posts, postmeta, terms/term_relationships/term_taxonomy tables (or their per-site equivalents on Multisite).Post typelh_tasks-task_postPost 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&#039;t be visible/claimable until reviewed.Workflow status ({$wpdb-&gt;prefix}lh_tasks_status table)Status key (namespace lh_tasks-)LabelNotesnew_taskNewInitial state; claimable (subject to post_status = lh_tasks-active)claim_taskClaimedSet atomically by claim_next_task(); renew_task() (the renew-claim heartbeat) keeps a task in this status while pushing claimed_at forwardprog_taskIn ProgressFor workers that separate &quot;claimed&quot; from &quot;started&quot;; not set automaticallypend_taskPendingManual/human-workflow use. Also set automatically (1.8.0) when a task waiting in New loses a blocked_by prerequisite to Cancelled, the trash or deletionrevw_taskAwaiting reviewOpen (1.23.0, #150480). Set when someone other than the task&#039;s author marks an assigned task done. The author or staff then accepts it (Resolved) or sends it back with a note (New, still assigned to the doer). Never claimable, and mark as done is refused while a task is in it.fail_taskFailedSet automatically when retries are exhausted, with the claim released (1.18.0; was block_task, &quot;Blocked&quot;, renamed so it is not confused with a task held back by a blocked_by prerequisite, which stays New and is skipped by claims). Open, for a person to triage; edit-task with status New and reset_retries requeues it with its full retriesres_taskResolvedSet by complete_task(); terminalclos_taskClosedManual final closure after resolution; terminalcncl_taskCancelledTerminaldefr_taskDeferredParked for later — deliberately worth doing eventually, not urgent now. Not in return_open_statuses() (won&#039;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&#039;t constrained the same way, but there was no reason to rename and break continuity with existing data/consumers. The one exception is block_task, renamed to fail_task in 1.18.0 because the old name was actively misleading.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.Table version 2.1 (1.18.0) changes data, not columns: migrate_2_0_to_2_1(), run once per site from maybe_create_table() on init, moves block_task rows to fail_task, and repairs two keys that edit-task stored before 1.17.1 validated statuses: can_task to cncl_task and resolved to res_task. These are plain row updates, so no progress-log entries are written and no status actions fire. The version is in the lh_tasks_status_table_version option.Dependencies ({$wpdb-&gt;prefix}lh_tasks_dependencies table, 1.8.0)One row per &quot;task waits on blocker&quot; edge, and the only store of blocked_by (it replaced the _lh_tasks-blocked_by post meta, which was migrated and deleted). Columns: task_id, blocker_id (primary key together), created_at; indexed on blocker_id. Version option lh_tasks_dependencies_schema_version. Rows for a permanently deleted task are removed in either column.Taxonomieslh_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.lh_tasks_priority (1.15.0) — a locked set of four terms (low, normal, high, urgent), ranked by term meta order (10, 20, 30, 40). Each task carries one. Version option lh_tasks_priority_schema_version. See Architecture notes.Post meta (all keys prefixed _lh_tasks-)Meta keyTypeDescriptionpayloadJSON stringMachine-actionable spec for agent tasks: the ability to run and its paramsresultJSON stringOutput on completion (since 1.23.0 the completion note, for a task marked done by a person), or an error object on a retryable failure. A stale error is cleared when the task is later completeddue_dateMySQL datetimeOptionalsource_pluginstringSlug of the plugin that created this task, e.g. lh-crmsource_post_idintID of the originating post in the source pluginskip_reviewint (1)Set when the task was created with requires_review false: the assignee&#039;s done resolves it without a review step. 1.23.0progress_logarray of objectsAppended to by reassign_task(), update_task(), add_note(), complete_assigned_task(), review_task(), LH_Tasks_dependencies_class::flag_dependents(), LH_Tasks_follow_up_class, renew_task() (only when a note is given), and edit_task(). Each entry: timestamp, event (reassigned, heartbeat, admin_override, prerequisite_dropped, note (1.10.0), marked_done (1.11.0, when the assigned member marks it done from their profile, with their note if they gave one), follow_up and follow_up_escalated (1.16.0, written by the daily follow-up with actor 0, and never counted as activity), submitted_for_review, review_accepted and review_sent_back (1.23.0, the review step, each with to_status), or updated, which also records fields and, if the task was claimed, claimed_by), from_status/to_status (reassigned, admin_override and prerequisite_dropped only), from_assignee/to_assigned_to (reassign only), retries_reset_from (admin_override with reset_retries, 1.18.0: the retry count before it was reset), via (1.23.0: mcp when a completion or review came through an ability rather than the task page), actor (WP user ID), notefollow_uparrayWhere an assigned task is in the follow-up cycle, including when the last follow-up was sent (at); activity after that time starts the cycle again. 1.16.0claim_startedUnix timestampWhen the current claim was made, set on lh_tasks_task_claimed; the start of the maximum lease for automatic renewal. 1.17.0ability_callsintCount of write ability calls made with this task (succeeded or failed, not refused). 1.17.0last_abilityarrayThe last such call: ability, time_gmt, success, renewed. 1.17.0assignee, assigned_to, claim_token, claimed_at, retry_count, and max_retries live in the status table, not postmeta — see above. The follow-up&#039;s BuddyPress notifications carry two notification meta keys, lh_tasks_blog_id and lh_tasks_link, recording the task&#039;s site and link (1.16.0)." ;
  sioc:topic <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/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
