{"id":41,"date":"2026-07-13T11:04:32","date_gmt":"2026-07-13T05:34:32","guid":{"rendered":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/"},"modified":"2026-07-13T11:05:51","modified_gmt":"2026-07-13T05:35:51","slug":"entryvault-adapters","status":"publish","type":"page","link":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/","title":{"rendered":"Writing an EntryVault adapter"},"content":{"rendered":"<p>An <a href=\"https:\/\/jnkplugins.com\/entryvault\/\" class=\"jnk-linkweave-auto\">EntryVault<\/a> adapter teaches the plugin how to capture submissions from one form builder. Each adapter is a small class implementing a three-method contract, then registered via a filter. This guide walks through writing one.<\/p>\n<p>You don&#8217;t need to modify EntryVault to add an adapter \u2014 register your class from your own plugin or your theme&#8217;s <code>functions.php<\/code> using the <code>entryvault_adapters<\/code> filter.<\/p>\n<h2>The contract<\/h2>\n<p>Every adapter extends <code>EntryVault\\Adapters\\Adapter_Base<\/code> and implements three abstract methods:<\/p>\n<pre><code>abstract public function slug(): string;         \/\/ stable id, e.g. \"my-forms\"\nabstract public function is_active(): bool;       \/\/ is the host plugin installed?\nabstract public function register_hooks(): void;  \/\/ hook the builder's submit event<\/code><\/pre>\n<p>The registry only calls <code>register_hooks()<\/code> on adapters whose <code>is_active()<\/code> returns true, so there&#8217;s zero overhead for builders that aren&#8217;t installed.<\/p>\n<h2>The normalised payload<\/h2>\n<p>Your hook callback&#8217;s job is to build one associative array and hand it to <code>dispatch()<\/code>. This is the shape EntryVault&#8217;s sync engine stores:<\/p>\n<pre><code>[\n  'source_plugin'   =&gt; $this-&gt;slug(),      \/\/ who captured it\n  'source_form_id'  =&gt; (string) $form_id,   \/\/ which form\n  'source_entry_id' =&gt; (string) $entry_id,  \/\/ which submission (see dedupe note)\n  'title'           =&gt; $title,              \/\/ card title\n  'email'           =&gt; $email,              \/\/ card email (nullable)\n  'fields'          =&gt; $fields,             \/\/ list of [key,label,type,value]\n  'created_at'      =&gt; current_time( 'mysql' ),\n]<\/code><\/pre>\n<h3>Dedupe: the one rule that matters<\/h3>\n<p>EntryVault de-duplicates on the triple <strong>(source_plugin, source_form_id, source_entry_id)<\/strong>. If the builder gives you a real, stable entry id, use it. If it stores nothing (as Contact Form 7 does), you <strong>must synthesise a unique id per submission<\/strong> \u2014 otherwise two unrelated submissions with the same id silently merge into one record.<\/p>\n<h2>Helpers the base class gives you<\/h2>\n<pre><code>$this-&gt;dispatch( $normalized );        \/\/ ingest the payload (idempotent)\n$this-&gt;pick_email( $fields );          \/\/ best-guess email from the fields\n$this-&gt;pick_title( $fields, $email );  \/\/ best-guess card title (name field, else email)<\/code><\/pre>\n<h2>A minimal adapter<\/h2>\n<p>A complete adapter for a hypothetical builder that fires <code>myforms_after_submit<\/code> with a form id and a <code>label =&gt; value<\/code> map:<\/p>\n<pre><code>namespace MyPlugin;\n\nuse EntryVault\\Adapters\\Adapter_Base;\n\nclass Adapter_My_Forms extends Adapter_Base {\n\n    public function slug(): string {\n        return 'my-forms';\n    }\n\n    public function is_active(): bool {\n        return defined( 'MYFORMS_VERSION' );\n    }\n\n    public function register_hooks(): void {\n        add_action( 'myforms_after_submit', [ $this, 'capture' ], 20, 2 );\n    }\n\n    public function capture( int $form_id, array $data ): void {\n        $fields = [];\n        foreach ( $data as $label =&gt; $value ) {\n            $fields[] = [\n                'key'   =&gt; sanitize_key( $label ),\n                'label' =&gt; $label,\n                'type'  =&gt; is_string( $value ) &amp;&amp; is_email( $value ) ? 'email' : 'text',\n                'value' =&gt; is_array( $value ) ? implode( ', ', $value ) : $value,\n            ];\n        }\n        $email = $this-&gt;pick_email( $fields );\n        $this-&gt;dispatch( [\n            'source_form_id'  =&gt; (string) $form_id,\n            'source_entry_id' =&gt; 'myforms_' . $form_id . '_' . uniqid( '', true ),\n            'title'           =&gt; $this-&gt;pick_title( $fields, $email ),\n            'email'           =&gt; $email,\n            'fields'          =&gt; $fields,\n        ] );\n    }\n}<\/code><\/pre>\n<h2>Registering it<\/h2>\n<p>Add your class to the candidate list through the <code>entryvault_adapters<\/code> filter \u2014 no core edits:<\/p>\n<pre><code>add_filter( 'entryvault_adapters', function ( array $adapters ) {\n    $adapters[] = \\MyPlugin\\Adapter_My_Forms::class;\n    return $adapters;\n} );<\/code><\/pre>\n<p>That&#8217;s the whole integration. On the next submission your adapter runs, the entry is normalised and stored, and it appears on the board alongside every other builder&#8217;s submissions.<\/p>\n<h2>Tips for metadata-poor builders<\/h2>\n<ul>\n<li><strong>No labels?<\/strong> Humanise the field key (e.g. <code>full_name<\/code> \u2192 &#8220;Full Name&#8221;) and infer a type from the value.<\/li>\n<li><strong>No pre-send hook?<\/strong> Capture as early as the builder allows so a failed notification email doesn&#8217;t cost you the lead.<\/li>\n<li><strong>No entry id?<\/strong> Synthesise one from the form id plus <code>microtime()<\/code> and <code>uniqid('', true)<\/code> \u2014 see the dedupe rule above.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>An EntryVault adapter teaches the plugin how to capture submissions from one form builder. Each adapter is a small class implementing a three-method contract, then registered \u2026<\/p>\n","protected":false},"author":0,"featured_media":0,"parent":5,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-41","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Writing an EntryVault adapter - JnK Plugins Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Writing an EntryVault adapter - JnK Plugins Blog\" \/>\n<meta property=\"og:description\" content=\"An EntryVault adapter teaches the plugin how to capture submissions from one form builder. Each adapter is a small class implementing a three-method contract, then registered \u2026\" \/>\n<meta property=\"og:url\" content=\"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/\" \/>\n<meta property=\"og:site_name\" content=\"JnK Plugins Blog\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-13T05:35:51+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"3 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/docs\\\/entryvault-adapters\\\/\",\"url\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/docs\\\/entryvault-adapters\\\/\",\"name\":\"Writing an EntryVault adapter - JnK Plugins Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-07-13T05:34:32+00:00\",\"dateModified\":\"2026-07-13T05:35:51+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/docs\\\/entryvault-adapters\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/docs\\\/entryvault-adapters\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/docs\\\/entryvault-adapters\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Documentation\",\"item\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/docs\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Writing an EntryVault adapter\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/\",\"name\":\"JnK Plugins Blog\",\"description\":\"Guides, comparisons, and tutorials for our WordPress plugins\",\"publisher\":{\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/#organization\",\"name\":\"JnK Plugins\",\"url\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/jnkplugins-jk-logo-512.png\",\"contentUrl\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/jnkplugins-jk-logo-512.png\",\"width\":512,\"height\":512,\"caption\":\"JnK Plugins\"},\"image\":{\"@id\":\"https:\\\/\\\/jnkplugins.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.youtube.com\\\/@JnKPlugins\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/jnkplugins\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Writing an EntryVault adapter - JnK Plugins Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/","og_locale":"en_US","og_type":"article","og_title":"Writing an EntryVault adapter - JnK Plugins Blog","og_description":"An EntryVault adapter teaches the plugin how to capture submissions from one form builder. Each adapter is a small class implementing a three-method contract, then registered \u2026","og_url":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/","og_site_name":"JnK Plugins Blog","article_modified_time":"2026-07-13T05:35:51+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"3 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/","url":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/","name":"Writing an EntryVault adapter - JnK Plugins Blog","isPartOf":{"@id":"https:\/\/jnkplugins.com\/blog\/#website"},"datePublished":"2026-07-13T05:34:32+00:00","dateModified":"2026-07-13T05:35:51+00:00","breadcrumb":{"@id":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/jnkplugins.com\/blog\/docs\/entryvault-adapters\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/jnkplugins.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Documentation","item":"https:\/\/jnkplugins.com\/blog\/docs\/"},{"@type":"ListItem","position":3,"name":"Writing an EntryVault adapter"}]},{"@type":"WebSite","@id":"https:\/\/jnkplugins.com\/blog\/#website","url":"https:\/\/jnkplugins.com\/blog\/","name":"JnK Plugins Blog","description":"Guides, comparisons, and tutorials for our WordPress plugins","publisher":{"@id":"https:\/\/jnkplugins.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/jnkplugins.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/jnkplugins.com\/blog\/#organization","name":"JnK Plugins","url":"https:\/\/jnkplugins.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/jnkplugins.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/jnkplugins.com\/blog\/wp-content\/uploads\/2026\/08\/jnkplugins-jk-logo-512.png","contentUrl":"https:\/\/jnkplugins.com\/blog\/wp-content\/uploads\/2026\/08\/jnkplugins-jk-logo-512.png","width":512,"height":512,"caption":"JnK Plugins"},"image":{"@id":"https:\/\/jnkplugins.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.youtube.com\/@JnKPlugins","https:\/\/www.linkedin.com\/company\/jnkplugins\/"]}]}},"_links":{"self":[{"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/pages\/41","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"replies":[{"embeddable":true,"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/comments?post=41"}],"version-history":[{"count":1,"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/pages\/41\/revisions"}],"predecessor-version":[{"id":50,"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/pages\/41\/revisions\/50"}],"up":[{"embeddable":true,"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/pages\/5"}],"wp:attachment":[{"href":"https:\/\/jnkplugins.com\/blog\/wp-json\/wp\/v2\/media?parent=41"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}