Skip to content
Browse Plugins

Forms Workflow Sync — Developer & Architecture Guide

Technical reference for maintaining and extending the plugin. It covers the architecture, data model, request/data flow, the extension points (hooks, the form integration contract, the REST/AJAX surface), the security model, and how the free/premium split is produced.

Everything here reflects the current codebase. Identifiers use the FormsWorkflowSync\ namespace, the forms_workflow_sync_* function prefix, the FORMS_WORKFLOW_SYNC_* constant prefix, and the forms-workflow-sync text domain/slug.


Architecture overview

The plugin is a namespaced, PSR-4 codebase organized into layers, with a single static service registry (PluginFactory) providing shared instances. There is no heavyweight framework; WordPress hooks are the wiring.

forms-workflow-sync.php        Bootstrap: constants, Freemius, helpers, autoload, lifecycle
src/
  Core/
    PluginFactory.php          Static service registry (singletons)
    Logger.php                 DB-backed logger with redaction
    CredentialStore.php        Encrypt/decrypt API tokens at rest
    Config.php                 Settings get/set/all/delete wrapper
  Database/
    MigrationManager.php       Versioned, tracked schema migrations
  Integrations/
    Forms/
      FormIntegrationInterface.php   The contract every form integration implements
      FormManager.php                Loads, registers, and queries integrations
      ContactForm7.php               Free integration
      GravityForms.php  WPForms.php  NinjaForms.php
      FluentForms.php   FormidableForms.php  WooCommerce.php   (premium; stripped from free build)
    Monday/
      MondayManager.php        Monday.com GraphQL client + submission processor
  REST/
    RestController.php         REST API under forms-workflow-sync/v1
  Admin/
    AdminController.php        Admin pages, AJAX handlers, admin-post handlers
assets/css/admin-pure.css      Admin UI styles
vendor/freemius/               Freemius SDK (licensing, updates, free/premium split)

Service registry

FormsWorkflowSync\Core\PluginFactory lazily creates and caches one instance of each service. Always resolve services through it rather than constructing them directly, so wiring stays consistent.

AccessorReturns
PluginFactory::getLogger()Core\Logger
PluginFactory::getMondayManager()Integrations\Monday\MondayManager
PluginFactory::getFormManager()Integrations\Forms\FormManager
PluginFactory::getRestController()REST\RestController
PluginFactory::getMigrationManager()Database\MigrationManager
PluginFactory::getAdminController()Admin\AdminController

Bootstrap & lifecycle

The main file defines constants (FORMS_WORKFLOW_SYNC_VERSION, _PLUGIN_FILE, _PLUGIN_DIR, _PLUGIN_URL, _PLUGIN_BASENAME), boots Freemius, registers helper functions, and sets up autoloading (Composer autoload if present, otherwise a spl_autoload_register fallback mapping FormsWorkflowSync\src/).

FormsWorkflowSyncPlugin is the lifecycle singleton:

  • Activation (register_activation_hook) → runs migrations, ensures capabilities, schedules the hourly retry cron (forms_workflow_sync/retry_failed_submissions), and (on multisite network activation) repeats per site.
  • Deactivation → clears the retry cron.
  • New site created (wp_initialize_site) → activates on the new site when the plugin is network-active.
  • Boot on plugins_loaded (priority 100): loads the text domain, ensures capabilities, runs migrations, then initializes the REST controller, admin controller, Monday manager, the submission handler, and the form manager, and finally fires do_action('forms_workflow_sync/loaded').

There is a PHP 8.0 floor: on older PHP the plugin shows an admin notice and returns before booting.


Data model

All tables are prefixed with the site’s $wpdb->prefix + forms_workflow_sync_. Schema is applied by MigrationManager via dbDelta, with each migration recorded in the _migrations table so it runs once.

_connections — one Monday.com account’s credentials. id, name, account_name, access_token (encrypted), token_type, workspace_id, workspace_name, is_active, created_at, updated_at.

_integrations — a form-to-board mapping. id, form_plugin, form_id, form_name, connection_id, board_id, group_id, field_mapping (JSON), is_active, timestamps. Indexed on (form_plugin, form_id) and connection_id.

_submissions — one captured submission per active mapping. id, integration_id, form_data (JSON), monday_item_id, status, error_message, created_at, processed_at. Indexed on integration_id and status.

_logsid, level, message, context (JSON), created_at.

_queue — retry bookkeeping. id, submission_id, retry_count, max_retries (default 3), next_retry, created_at.

_migrationsid, migration, ran_at.

Relationships: an integration references a connection; a submission references an integration; a queue row references a submission.


Request / data flow

The capture-to-sync path is deliberately decoupled through actions, so each stage is independently hookable.

[Form plugin native submit hook]
        │  (e.g. wpcf7_mail_sent)
        ▼
Integration handler  ──build standardized array──▶  handle_submission()
        │                                                   │
        │                       do_action('forms_workflow_sync/form_submission', $form_data)
        ▼
Submission handler (main file, priority 1)
        │  looks up active mappings for (form_id, form_plugin)
        │  sanitizes payload, INSERTs a _submissions row (status 'received')
        ▼
        do_action('forms_workflow_sync/process_submission', $submission_id, $payload)
        ▼
MondayManager::process_submission()
        │  builds GraphQL, calls create_item() against api.monday.com/v2
        ▼
_submissions row updated (monday_item_id, status, processed_at)
        │  on failure → retry queue (premium)
        ▼
Hourly cron 'forms_workflow_sync/retry_failed_submissions' → MondayManager::retry_failed_submissions()

The standardized submission array produced by every integration is:

php

[
    'form_plugin'     => 'Contact Form 7',   // must equal the integration's get_plugin_name()
    'form_id'         => '42',               // string
    'form_name'       => 'Contact form',
    'submission_data' => [ /* field name => value */ ],
]

Extension point: adding a form integration

Every form integration implements FormsWorkflowSync\Integrations\Forms\FormIntegrationInterface:

php

interface FormIntegrationInterface
{
    public function get_plugin_name(): string;                       // display name & matching key
    public function get_forms(): array;                              // [ ['id'=>..,'name'=>..], ... ]
    public function get_form_fields(string $form_id): array;         // [ ['id'=>..,'label'=>..,'type'=>..], ... ]
    public function register_hooks(): void;                          // hook the form plugin's submit event
    public function handle_submission(array $form_data): void;       // fire the standardized submission
}

An optional public function is_available(): bool is honored if present — FormManager skips the integration when it returns false (use it to check that the third-party form plugin is installed).

Contract responsibilities

  • register_hooks() should attach to the form plugin’s own submission action and, in the handler, assemble the standardized array and call handle_submission().
  • handle_submission() must fire do_action('forms_workflow_sync/form_submission', $form_data) — that is what hands the submission to the rest of the pipeline.
  • get_forms() / get_form_fields() power the admin mapping UI. Return IDs as strings.
  • Guard everything with your availability check so a missing form plugin can’t fatal the site.

Registering a custom integration

FormManager fires forms_workflow_sync/register_form_integrations (passing itself) after loading the built-ins. Register from a third-party plugin like this:

php

add_action('forms_workflow_sync/register_form_integrations', function ($manager) {
    $manager->register_integration('my-forms', new My_Forms_Integration(
        \FormsWorkflowSync\Core\PluginFactory::getLogger()
    ));
});

register_integration() honors the optional is_available() check and keys the integration by a sanitized slug. To add an integration to the plugin’s own free tier, add it to the free $integration_classes array in FormManager::load_integrations(); to add it to the premium tier, add it inside the can_use_premium_code__premium_only() block (see the free/premium section below).


Public hooks reference

Actions

ActionArgsFired when
forms_workflow_sync/loadedBoot completes.
forms_workflow_sync/register_form_integrationsFormManager $managerAfter built-in integrations load; register your own here.
forms_workflow_sync/form_submissionarray $form_dataAn integration captured a submission (standardized array).
forms_workflow_sync/process_submissionint $submission_id, array $payloadA submission row was stored and is ready to sync.
forms_workflow_sync/retry_failed_submissionsHourly cron event (drives the premium retry queue).
fws_fs_loadedFreemius SDK finished initializing.

Filters

FilterArgsPurpose
plugin_action_links_{basename}array $linksAdds the “Settings” link on the Plugins screen.

Helper functions (global)

FunctionPurpose
forms_workflow_sync_can_use_premium(): boolSafe premium check for shared code paths — wraps fws_fs()->can_use_premium_code(). Present in both builds.
forms_workflow_sync_user_can_manage(): boolCentral capability check used by admin, AJAX, and REST.
forms_workflow_sync_sanitize_submission_value($value, int $depth = 0)Recursively sanitizes/limits submission values (depth ≤ 5, ≤ 200 array items, ≤ 10 000 chars per scalar).
fws_fs()Returns the Freemius instance.

Capabilities

manage_forms_workflow_sync and view_forms_workflow_sync_submissions are granted to the administrator role on boot/activation. forms_workflow_sync_user_can_manage() also accepts manage_options (and manage_network_options on multisite).


REST API

Namespace: forms-workflow-sync/v1. Every route uses a permission callback that enforces the manage capability, so requests need an authenticated, capable user (cookie + nonce, or application password).

MethodRoutePurpose
GET / POST/connectionsList / create connections.
GET / … / DELETE/connections/{id}Read / update / delete a connection.
GET / POST/integrationsList / create mappings.
GET / … / DELETE/integrations/{id}Read / update / delete a mapping.
GET/workflow/{resource}Board-level lookups (e.g. boards).
GET/workflow/boards/{board_id}/groupsGroups in a board.
GET/workflow/boards/{board_id}/columnsColumns in a board.
GET/formsAvailable forms across active integrations.
GET/logsRecent log entries.
GET/submissionsRecent submissions.

Create/update on connections and integrations enforce the free-tier limits server-side and return HTTP 402 when a limit is exceeded on the free build.


AJAX & admin-post handlers

Registered by AdminController. AJAX handlers assert nonce + capability; admin-post handlers check capability + check_admin_referer.

AJAX (wp_ajax_…): forms_workflow_sync_get_forms, forms_workflow_sync_get_board_data, forms_workflow_sync_get_workspaces, forms_workflow_sync_get_boards.

admin-post (admin_post_…): forms_workflow_sync_clear_logs, forms_workflow_sync_delete_connection, forms_workflow_sync_delete_integration.

Connection and integration creation are handled as POSTs to the admin pages themselves (handle_add_connection / handle_add_integration), which also enforce the free-tier limits.


Monday.com client (MondayManager)

Talks to the hardcoded endpoint https://api.monday.com/v2 via wp_remote_post. Key public methods:

MethodPurpose
test_connection($token)Validate a token.
get_workspaces($token)List workspaces.
get_boards($token, $workspace_id = null) / get_boards_by_workspace(...)List boards.
get_board_groups($token, $board_id)Groups in a board.
get_board_columns($token, $board_id)Columns in a board.
create_item(...)Create a board item from a mapped submission.
make_api_call($token, $query)Low-level GraphQL POST.
process_submission($submission_id, $form_data)Hooked to process_submission; performs the sync and updates status.
retry_failed_submissions()Hooked to the hourly cron; re-attempts failed sends (premium).

GraphQL values are escaped through a private escape_string() helper before interpolation. The endpoint is fixed (no user-controlled host), redirects are off, and TLS verification is on — there is no SSRF surface.


Security model

  • Credentials at rest: Core\CredentialStore::encrypt()/decrypt() encrypt API tokens before they touch the database. It prefers libsodium (sodium_crypto_secretbox, ciphertext prefixed s:) and falls back to OpenSSL AES (o: prefix); the key derives from WordPress salts. is_encrypted() guards the one-time migration that encrypts any legacy plaintext tokens. Tokens are never returned to the browser.
  • Authorization everywhere: every AJAX handler checks nonce + capability, every admin-post handler checks capability + referer, and every REST route has a permission callback. All funnel through the manage capability.
  • SQL: all user-influenced queries use $wpdb->prepare(). The only interpolated identifiers are $wpdb->prefix table names.
  • Submission hygiene: inbound submission data is normalized by forms_workflow_sync_sanitize_submission_value() (bounded depth, item count, and length) before storage or transmission.
  • Logging: sensitive values are redacted before they are written to the log table.

When extending, preserve these invariants: prepare every query, check capability on every new endpoint, never echo a token, and route new outbound calls through the fixed Monday.com client rather than opening new HTTP surfaces.


Free vs premium: how the split is produced

The plugin is a single freemium codebase. The Freemius deployment preprocessor generates two zips from one upload: the premium zip (all code) and the free zip (premium code removed). Two mechanisms do the removal:

  1. Conditional block stripping. Any if ( fws_fs()->..._premium_only() ) { … } block is deleted from the free build. FormManager::load_integrations() uses this: the six premium integration classes are only registered inside if (fws_fs()->can_use_premium_code__premium_only()) { … }, so the free build never loads them. The retry queue methods gate on forms_workflow_sync_can_use_premium() at runtime.
  2. File stripping via the plugin header. The main file’s header carries an @fs_premium_only directive listing the six premium integration class files, so the preprocessor physically removes them from the free build (WordPress.org requires that no premium code ship in the hosted free version). Any non-stripped code path that references those classes is guarded with class_exists() so the free build resolves a fallback instead of fataling.

The two premium checks — use the right one

  • forms_workflow_sync_can_use_premium() / fws_fs()->can_use_premium_code() — a real method present in both builds. Safe to call from shared code (admin, REST, MondayManager). Returns false in the free build and in the premium build without a valid license/trial.
  • fws_fs()->can_use_premium_code__premium_only() — the __premium_only variant. Its purpose is to mark a block for stripping; only ever call it inside a block you intend the preprocessor to remove from the free build (as FormManager does). Do not call it from code that must run in the free build.

Where the tier limits live

Free-tier caps (Contact Form 7 only, one connection, one active mapping) are enforced at each create path — AdminController::handle_add_connection / handle_add_integration, and RestController create routes (HTTP 402) — using the shared forms_workflow_sync_can_use_premium() helper. Keep enforcement at the create boundary so existing data is never silently dropped.


Adding a database migration

Add a numbered entry to MigrationManager::register_migrations() mapping a unique key to a private method, then implement that method with idempotent dbDelta (for new tables) or guarded ALTER TABLE (for column changes; check DESC {$table} first, as update_connections_table() does). Migrations run once and are recorded in the _migrations table; never rename or renumber an existing migration key.


Conventions

  • Namespace FormsWorkflowSync\, function prefix forms_workflow_sync_, constant prefix FORMS_WORKFLOW_SYNC_, text domain / slug forms-workflow-sync.
  • Internal references to Monday.com (the Integrations\Monday namespace, MondayManager, the monday_item_id column, api.monday.com) are intentional and accurate — they are not public branding and should stay.
  • Resolve shared services through PluginFactory; add new cross-cutting services there.
  • Prefer decoupling via the existing action pipeline over direct calls between layers.