{"id":98099,"date":"2026-10-13T19:50:00","date_gmt":"2026-10-13T23:50:00","guid":{"rendered":"https:\/\/overcentral.com\/en\/?p=98099"},"modified":"2026-09-29T08:06:45","modified_gmt":"2026-09-29T12:06:45","slug":"emdash-hook-execution-order-98099","status":"publish","type":"post","link":"https:\/\/overcentral.com\/en\/emdash-hook-execution-order-98099\/","title":{"rendered":"EmDash Hook Execution Order: Why Sequential Logic Is a Trap"},"content":{"rendered":"<p>Every WordPress developer knows the rule. Hook priority determines execution order. <code>add_action( 'init', 'my_func', 10 );<\/code>codecodecodecode runs before <code>add_action( 'init', 'my_func', 11 );<\/code>codecodecodecode. It&#8217;s predictable. It&#8217;s reliable. It&#8217;s the bedrock of countless plugin architectures.<\/p>\n<p>That advice is dangerous in <a href=\"https:\/\/emdash.dev\/\" target=\"_blank\" rel=\"noopener noreferrer\" data-iacss-external=\"1\">EmDash<\/a>.<\/p>\n<p>The conventional wisdom about hook order doesn&#8217;t just shift in Cloudflare&#8217;s new CMS. It breaks entirely. If you&#8217;re migrating plugins or building new ones, assuming sequential execution will produce race conditions, silent failures, and data corruption that are nearly impossible to debug.<\/p>\n<p>Here&#8217;s why\u2014<a href=\"https:\/\/overcentral.com\/en\/star-wars-zero-company-system-requirements-78460\/\" title=\"Why Checking Star Wars Zero Company System Requirements Isn\u2019t Enough (And What to Do Instead)\" data-iacss-internal=\"1\">and what to do<\/a> about it.<\/p>\n<h2>The WordPress Assumption \u2014 The Priority Queue Mentality<\/h2>\n<p>WordPress uses a global hook registry stored in memory. When <code>do_action( 'posts:published' )<\/code>codecodecodecode fires, the system walks through every registered callback sorted by a numeric priority parameter. Within the same priority level, callbacks execute sequentially in the order they were added.<\/p>\n<p>This creates a deterministic chain. Plugin A finishes. Plugin B starts. Plugin B finishes. Plugin C starts. You can trace the exact path of execution through your entire plugin stack.<\/p>\n<p>Developers exploit this constantly. A caching plugin hooks into <code>save_post<\/code>codecodecodecode at priority 10. An SEO plugin hooks into the same event at priority 15, knowing the cache was already cleared. The system guarantees that ordering. It&#8217;s not a convention\u2014it&#8217;s an architectural promise.<\/p>\n<p>EmDash makes no such promise.<\/p>\n<h2>How EmDash Actually Executes Hooks \u2014 The Isolate Paradigm<\/h2>\n<p>EmDash is built on <a href=\"https:\/\/workers.cloudflare.com\/\" target=\"_blank\" rel=\"noopener noreferrer\" data-iacss-external=\"1\">Cloudflare Workers<\/a> and V8 isolates. When a hook like <code>posts:published<\/code>codecodecodecode fires, EmDash doesn&#8217;t walk through a global PHP array. It dispatches an event to every registered plugin. Each plugin&#8217;s callback runs inside its own Dynamic Worker\u2014a completely isolated V8 context.<\/p>\n<p>These workers spin up in milliseconds, execute the callback, and spin back down. From the perspective of the main application thread, they run <em>in parallel<\/em>.<\/p>\n<p>There is no global priority queue. There is no shared execution context. Plugin A and Plugin B, both listening to the same hook, execute concurrently. The WordPress advice\u2014&#8221;set your priority to 10 or 20 to control order&#8221;\u2014is not just wrong in EmDash. It&#8217;s a dangerous illusion.<\/p>\n<p>The <code>priority<\/code>codecodecodecode parameter in EmDash&#8217;s <code>definePlugin<\/code>codecodecodecode function is effectively decorative for cross-plugin ordering. It controls the order plugins are <em>registered<\/em> in the system, not the order they <em>execute<\/em>. Two workers cannot wait on each other without explicit orchestration.<\/p>\n<h2>The <code>posts:published<\/code> Hook \u2014 A Concrete Example<\/h2>\n<p>The source material for EmDash shows a plugin that sends an email when a post is published. It declares two capabilities: <code>read_content<\/code>codecodecodecode and <code>email_sent<\/code>codecodecodecode. The hook is <code>posts:published<\/code>codecodecodecode.<\/p>\n<p>Imagine a second plugin that logs the publication to an external analytics service. Both plugins hook into the same event. In WordPress, you&#8217;d set the logging plugin to priority 5 and the email plugin to priority 10. The log writes first. The email sends second. Ordered. Predictable.<\/p>\n<p>In EmDash, both plugins receive the event simultaneously. The log worker spins up, reads the post content, and makes an HTTP request to your analytics endpoint. The email worker spins up, reads the post content, and sends an SMTP message.<\/p>\n<p>Neither worker knows the other exists. The log might complete first. The email might complete first. The HTTP request from the log worker might fail because the email worker&#8217;s outbound call consumed a shared resource limit. These are race conditions you cannot reproduce in a local testing environment that uses a single-threaded Node.js process.<\/p>\n<p>The deeper problem is that many plugins implicitly depend on state set by other plugins. An SEO plugin might write meta fields during <code>posts:published<\/code>codecodecodecode. A social-sharing plugin might read those fields during the same hook. In WordPress, the execution order guarantees the fields exist. In EmDash, the social-sharing plugin might read stale or empty values.<\/p>\n<h2>When Order Actually Matters (And How to Enforce It)<\/h2>\n<p>The most common scenario where order matters is a cascading operation: update a post meta field, regenerate a sitemap, purge the CDN cache. In WordPress, this is a simple hook chain with increasing priority values. In EmDash, three isolated workers can step on each other.<\/p>\n<p>The fix is counterintuitive. Don&#8217;t try to enforce cross-plugin ordering. Instead, build a single orchestrator plugin that sequences its own internal operations.<\/p>\n<p>EmDash provides hooks through the <code>definePlugin<\/code>codecodecodecode function, but you can also use standard JavaScript <code>async\/await<\/code>codecodecodecode patterns inside a single plugin. A single Dynamic Worker runs your entire callback sequentially. If you need to update meta, regenerate a sitemap, and purge cache, do it in one plugin with three internal steps.<\/p>\n<p>&#8220;`javascript<\/p>\n<p>definePlugin({<\/p>\n<p>  id: &#8216;my-orchestrator&#8217;,<\/p>\n<p>  capabilities: [&#8216;read_content&#8217;, &#8216;write_content&#8217;, &#8216;cache_purge&#8217;],<\/p>\n<p>  hooks: {<\/p>\n<p>    &#8216;posts:published&#8217;: async ({ context, post }) =&gt; {<\/p>\n<p>      \/\/ Step 1: Update meta<\/p>\n<p>      await context.updateMeta(post.id, { sitemap_changed: true });<\/p>\n<p>      \/\/ Step 2: Regenerate sitemap<\/p>\n<p>      await context.regenerateSitemap();<\/p>\n<p>      \/\/ Step 3: Purge cache<\/p>\n<p>      await context.purgeCDN();<\/p>\n<p>    }<\/p>\n<p>  }<\/p>\n<p>});<\/p>\n<p>&#8220;`<\/p>\n<p>This runs inside a single isolate. No parallel execution. No race conditions. No dependency on other plugins.<\/p>\n<p>If you must coordinate across multiple plugins, use EmDash&#8217;s internal event bus or a shared D1 database table as a coordination layer. One plugin writes a completion marker. The next plugin reads it. This is slower than WordPress&#8217;s shared memory model, but it&#8217;s safe.<\/p>\n<p>A hidden variable here is the file system. EmDash loads plugins based on file globs in the <code>src\/plugins\/<\/code>codecodecodecode directory. Unlike WordPress&#8217;s database-driven plugin registry, the order your operating system returns file names\u2014which varies by filesystem, OS, and even directory fragmentation\u2014influences the order plugins are <em>registered<\/em>. This makes relying on registration order even more fragile than it already is.<\/p>\n<h2>Testing for Hook Execution Order<\/h2>\n<p>The EmDash playground is useful for exploring the admin interface. It&#8217;s not useful for testing race conditions.<\/p>\n<p>You need integration tests that account for non-deterministic execution. Use <code>Promise.allSettled()<\/code>codecodecodecode patterns and assert on final system state, not on the sequence of side effects. A test that passes because Plugin A ran first today might fail tomorrow.<\/p>\n<p>&#8220;`javascript<\/p>\n<p>\/\/ Bad test: assumes execution order<\/p>\n<p>test(&#8217;email sends after log&#8217;, async () =&gt; {<\/p>\n<p>  await triggerPublish();<\/p>\n<p>  expect(logSpy).toHaveBeenCalled();<\/p>\n<p>  expect(emailSpy).toHaveBeenCalled(); \/\/ May fail if email ran first<\/p>\n<p>});<\/p>\n<p>\/\/ Good test: asserts final state<\/p>\n<p>test(&#8216;both plugins complete&#8217;, async () =&gt; {<\/p>\n<p>  await triggerPublish();<\/p>\n<p>  await new Promise(r =&gt; setTimeout(r, 2000)); \/\/ Let isolates settle<\/p>\n<p>  expect(await getLogCount()).toBe(1);<\/p>\n<p>  expect(await getEmailCount()).toBe(1);<\/p>\n<p>});<\/p>\n<p>&#8220;`<\/p>\n<p>The EmDash CLI includes a <code>test<\/code>codecodecodecode command that spins up a local D1 database and isolates. Use it. Run your tests multiple times in rapid succession. If you ever see a flaky failure, you have an ordering dependency you need to collapse into a single plugin.<\/p>\n<h2>The AI Agent Wildcard<\/h2>\n<p>EmDash ships with a built-in MCP server. This means <a href=\"https:\/\/overcentral.com\/en\/rogue-ai-agents-liability-vacuum-97898\/\" title=\"Rogue AI agents expose liability vacuum as OpenAI faces claims\" data-iacss-internal=\"1\">AI agents<\/a> can trigger hooks programmatically. An agent might call <code>posts:published<\/code>codecodecodecode directly, bypassing the normal UI flow.<\/p>\n<p>When an agent triggers a hook, it doesn&#8217;t respect any implicit ordering you might have built around user workflows. The agent fires the event, and all registered workers spin up in parallel. If your plugins depend on a specific chain of side effects, an agent can silently break that chain.<\/p>\n<p>The solution is the same: single-plugin orchestration with explicit <code>async\/await<\/code>codecodecodecode sequencing. Design your plugins as if every hook call is the first and only call. Stateless. Idempotent. Independent.<\/p>\n<p>EmDash&#8217;s architecture isn&#8217;t a bug. It&#8217;s a feature that forces you to write plugins that are truly isolated. That&#8217;s a better foundation for a secure, scalable CMS than WordPress&#8217;s shared-everything model. But it requires unlearning the priority queue mentality.<\/p>\n<p>Let go of the sequence. Embrace the isolate.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Every WordPress developer knows the rule. Hook priority determines execution order. add_action( &#8216;init&#8217;, &#8216;my_func&#8217;, 10 );codecodecodecode runs before add_action( &#8216;init&#8217;, &#8216;my_func&#8217;, 11 );codecodecodecode. It&#8217;s predictable. It&#8217;s reliable. It&#8217;s the bedrock of countless plugin architectures. That advice is dangerous in EmDash. The conventional wisdom about hook order doesn&#8217;t just shift in Cloudflare&#8217;s new CMS. It breaks [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":100456,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"fifu_image_url":"https:\/\/cards.overcentral.com\/cards\/en\/98099.png","fifu_image_alt":"EmDash Hook Execution Order: Why Sequential Logic Is a Trap","footnotes":""},"categories":[31],"tags":[],"class_list":["post-98099","post","type-post","status-publish","format-standard","has-post-thumbnail","category-technology"],"fifu_image_url":"https:\/\/cards.overcentral.com\/cards\/en\/98099.png","fifu_image_alt":"EmDash Hook Execution Order: Why Sequential Logic Is a Trap","_links":{"self":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98099","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/comments?post=98099"}],"version-history":[{"count":1,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98099\/revisions"}],"predecessor-version":[{"id":100457,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98099\/revisions\/100457"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/media\/100456"}],"wp:attachment":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/media?parent=98099"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/categories?post=98099"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/tags?post=98099"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}