JetFormBuilder: Send Email to a Related CCT Email Field via Relation

JetFormBuilder's Send Email action can't follow a JetEngine relation by itself. To send email to a related CCT email field, you first put the address into a form field. Build a JetEngine SQL/AI query that finds the CCT item related to the current post, turn it into a macro, load it into a Hidden Field, and set Send Email's MAIL TO to Email from submitted form field.
The hidden field travels through the visitor's browser, so we also sign the address on the server and check that signature on submission. If someone edits the recipient, the form stops before the vendor email goes out. Our example is an inquiry form on a single product page that emails the vendor whose CCT record is related to that product. The same setup works for any CPT and CCT connected by a JetEngine relation.
- Why Send Email can't read the related CCT on its own
- What you need
- Step 1: Query the related CCT item's email
- Step 2: Turn the query into a macro
- Step 3: Add the macro and verification snippet
- Step 4: Set up the form and actions
- Step 5: Test before going live
- Limits
- Troubleshooting
Why Send Email can't read the related CCT on its own
The Send Email action has three MAIL TO options: Admin email, Email from submitted form field and Custom email. None of them looks up related records, so the address has to reach a form field first.
There are two documented ways to use that field:
- The Send Email doc puts a JetEngine shortcode in a Hidden Field's DEFAULT VALUE, then types that field's name into Custom email's EMAIL ADDRESS.
- Crocoblock's related user recipe uses Email from submitted form field and picks the Hidden Field in FROM FIELD.
This tip follows the KB recipe. That recipe targets a WordPress user, though, and a CCT item needs a different query (Step 1). Feature request #8627 is still open, but it only asks for JetEngine Options Page values in Custom email > Email Address, so it wouldn't cover related CCT data either.
What you need
- A single template for your CPT (for example, Products). Crocoblock's recipe builds it with Elementor (Free).
- JetEngine with a CCT that has an email field, plus a Relation between that CCT and the CPT.
- JetFormBuilder, with the inquiry form placed on the CPT's single template.
- A code snippets plugin (such as Code Snippets) or a child theme.
Open the relation in JetEngine and note which side is the parent and which is the child. Step 1 depends on it.
Step 1: Query the related CCT item's email
- Go to WordPress Dashboard > JetEngine > Query Builder > Add New and name the query, for example "Related vendor email".
- Set Query Type to SQL/AI Query.
- In From table, pick your CCT's table.
- Turn on Use Join. Under Join Tables > Add new, set Join Type to Inner Join and Join Table to jet_rel_default. This is JetEngine's default relations table, with
parent_object_idandchild_object_idcolumns. If your relation is set to store its rows in its own table, check the relation settings and pick that table instead. - Set When current table column to the relation column from the table below. Set Is equal to other table column to your CCT's item ID column. This is the same order as Crocoblock's recipe, which puts
parent_object_idfirst and the From table'spost_idsecond. - In the WHERE (query clauses) section, add a clause:
- Column: the WHERE column from the table below.
- Compare: Equal (=).
- Value: click the Dynamic Tag icon and pick the tag that returns the current post's ID, not Current user ID. Check its exact label in your JetEngine version.
- If several relations share the table, check your relation settings for how to limit rows to this relation. Add that as a second clause with Where Relation set to And.
- If your version's SQL/AI query offers a limit setting, set it to 1. Click Add Query.
| Relation setup | When current table column | WHERE column = current post ID |
|---|---|---|
| CCT is parent, CPT is child | parent_object_id | jet_rel_default.child_object_id |
| CPT is parent, CCT is child | child_object_id | jet_rel_default.parent_object_id |
If you swap the direction, the query can return nothing or the wrong address, because CCT item IDs and post IDs are both plain numbers. This mapping follows the logic of Crocoblock's user recipe, so confirm it with the developer-tools check in Step 5 before you rely on it.
For the "CCT is parent" case, the settings produce roughly this SQL. Uppercase words are placeholders, and you don't type this anywhere:
SELECT CCT_TABLE.EMAIL_FIELD
FROM CCT_TABLE
INNER JOIN jet_rel_default
ON jet_rel_default.parent_object_id = CCT_TABLE.CCT_ID_COLUMN
WHERE jet_rel_default.child_object_id = CURRENT_POST_IDIf the query returns nothing, work through our list of common JetEngine Query Builder mistakes.
Step 2: Turn the query into a macro
- Go to WordPress Dashboard > JetEngine > JetEngine > Macros Generator.
- Set Macros to Query Results and Query to your new query.
- Set Return to List of selected fields from item object.
- In Comma-separated fields list, enter your CCT email field's name, not its label.
- Copy the generated macro for Step 4.
Step 3: Add the macro and verification snippet
A Hidden Field set to Manual Input returns its value as typed, so JetEngine macros don't run there by default. Crocoblock's macros-in-hidden gist fixes that for every hidden field on the site. Our snippet starts from the gist but only acts on one form, and it does three things:
- It resolves the macro in
vendor_email. The field gets a valid address or stays empty. - It writes an HMAC signature of that address into a second Hidden Field,
vendor_sig. The signature is made with your site's secret auth salt. - On submission, a Call Hook action checks the posted address against its signature. If they don't match, it stops the form before the vendor email is sent.
Add the snippet to your child theme's functions.php file, or as a new PHP snippet in Code Snippets, without the opening <?php tag. Change 123 to your form's ID: open the form in the editor and copy the number after post= in the address bar. Change the field names too if yours differ.
Code
/**
* Send JetFormBuilder inquiries to the email stored in a related JetEngine CCT item,
* with a server-side check so a visitor can't swap in another address.
*
* 1. Render: resolves the JetEngine macro in two Hidden Fields of ONE form.
* vendor_email gets the related address, vendor_sig gets a signature of it.
* 2. Submit: a Call Hook action (hook name bmc_check_vendor), placed above the
* vendor Send Email action, stops the submission when they don't match.
*
* Based on Crocoblock's macros-in-hidden gist. Re-run the Step 5 tests after
* every JetFormBuilder or JetEngine update.
*/
function bmc_vendor_settings() {
return array(
'form_id' => 123, // Your form's ID. Update it after duplicating or importing the form.
'email_field' => 'vendor_email', // Hidden Field that holds the recipient.
'sig_field' => 'vendor_sig', // Hidden Field that holds the signature.
);
}
function bmc_vendor_sign( $email, $form_id ) {
return hash_hmac( 'sha256', strtolower( $email ) . '|' . (int) $form_id, wp_salt( 'auth' ) );
}
function bmc_vendor_resolve_email( $value ) {
if ( ! function_exists( 'jet_engine' ) ) {
error_log( 'bmc_vendor: JetEngine is not loaded, vendor_email left empty.' );
return '';
}
$jet_engine = jet_engine();
if ( ! is_string( $value ) || ! isset( $jet_engine->listings->macros ) || ! is_object( $jet_engine->listings->macros ) ) {
return '';
}
$resolved = $jet_engine->listings->macros->do_macros( $value );
if ( ! is_string( $resolved ) ) {
return '';
}
// If the macro ever returns several values, keep only the first one.
$parts = preg_split( '/[\s,;]+/', trim( $resolved ), -1, PREG_SPLIT_NO_EMPTY );
$candidate = ! empty( $parts ) ? $parts[0] : '';
return is_email( $candidate ) ? $candidate : '';
}
// Render time: fill vendor_email and vendor_sig in the one configured form.
function bmc_vendor_render_fields( $args, $render = null ) {
$settings = bmc_vendor_settings();
if ( ! is_object( $render ) || ! isset( $render->form_id ) || $settings['form_id'] !== (int) $render->form_id ) {
return $args;
}
$name = isset( $args['name'] ) ? $args['name'] : '';
if ( $settings['email_field'] !== $name && $settings['sig_field'] !== $name ) {
return $args;
}
$email = bmc_vendor_resolve_email( isset( $args['field_value'] ) ? $args['field_value'] : '' );
if ( $settings['email_field'] === $name ) {
$args['field_value'] = $email;
} else {
$args['field_value'] = '' !== $email ? bmc_vendor_sign( $email, $settings['form_id'] ) : '';
}
return $args;
}
add_filter( 'jet-form-builder/render/hidden-field', 'bmc_vendor_render_fields', 10, 2 );
// Submit time: runs from the Call Hook action with hook name bmc_check_vendor.
function bmc_vendor_check_submission( $request = null ) {
$settings = bmc_vendor_settings();
$valid = false;
if ( is_array( $request ) ) {
$email_key = $settings['email_field'];
$sig_key = $settings['sig_field'];
$email = isset( $request[ $email_key ] ) && is_string( $request[ $email_key ] ) ? $request[ $email_key ] : '';
$sig = isset( $request[ $sig_key ] ) && is_string( $request[ $sig_key ] ) ? $request[ $sig_key ] : '';
if ( '' === $email ) {
// No related vendor: there is no vendor recipient to protect.
$valid = true;
} elseif ( is_email( $email ) && '' !== $sig && hash_equals( bmc_vendor_sign( $email, $settings['form_id'] ), $sig ) ) {
$valid = true;
}
}
if ( $valid ) {
return;
}
// Stop here so the vendor Send Email action below this Call Hook never runs.
if ( class_exists( '\Jet_Form_Builder\Exceptions\Action_Exception' ) ) {
throw new \Jet_Form_Builder\Exceptions\Action_Exception( 'failed' );
}
wp_die( esc_html__( 'This submission could not be verified.', 'bmc' ), '', array( 'response' => 403 ) );
}
add_action( 'jet-form-builder/custom-action/bmc_check_vendor', 'bmc_vendor_check_submission', 10, 1 );How the snippet works
JetFormBuilder applies jet-form-builder/render/hidden-field while it renders each Hidden Field. It passes the field's settings in $args (including name and field_value) and the renderer object, which carries form_id. The function works like this:
- It skips other forms and fields.
- It runs
do_macros()on the typed value, keeps the first value and accepts it only if it passesis_email(). - It signs that address with
hash_hmac(). - It always returns
$args, because the return value replaces the field's settings.
On submission, JetFormBuilder's Call Hook action fires jet-form-builder/custom-action/ followed by your hook name and passes the submitted values. The check signs the posted address again and compares the result with hash_equals(). A visitor can't produce a valid signature without your salt. If the signature doesn't match, the snippet throws JetFormBuilder's action exception, so the actions below Call Hook don't run. If that class is missing, wp_die() stops the request instead. An empty vendor_email passes the check, because there's no vendor recipient to protect.
Step 4: Set up the form and actions
- Add a Hidden Field named
vendor_email. Set FIELD VALUE to Manual Input and paste the Step 2 macro into VALUE. If VALUE is empty, JetFormBuilder falls back to the field's default or preset value, so keep the macro there. - Add a second Hidden Field named
vendor_sig, also set to Manual Input, with the same macro in VALUE. - On both fields, keep the render toggle on, which is the default. It's labeled Render in HTML in JetFormBuilder's docs, but check the exact label in your version. With it off, the field isn't rendered and the snippet never runs.
- Optional: add a Hidden Field with FIELD VALUE set to Current Post ID, so each record shows which product the inquiry came from. It's browser-editable too, so treat it as a label, not proof.
- Add the post-submit actions in this order:
- Send Email with MAIL TO set to Admin email. You get a copy of every inquiry, including ones for products without a vendor and ones the check rejects.
- Call Hook with the hook name
bmc_check_vendor. - Send Email with MAIL TO set to Email from submitted form field and FROM FIELD set to
vendor_email. Set REPLY TO to Email from submitted form field and pick the visitor's email field. Fill in SUBJECT and CONTENT, using the wrench icon to insert the visitor's name and message.
Send Email runs on every submission unless you add conditions with the criss-crossed arrows icon. If your version's conditions can check that vendor_email is not empty, add that condition to the vendor action.
Step 5: Test before going live
- Put a test address in a vendor's CCT record and open a related product. In developer tools,
vendor_emailmust hold exactly that address andvendor_siga long hex string. This also confirms your relation direction. You can temporarily add a visible Text Field with the same macro to see the resolved address on the page. - Submit a test inquiry. Check that the vendor inbox and the admin inbox both got the email, and that Form Records stores the right
vendor_email. - Repeat on a product related to a different vendor and confirm the address changes.
- Run the tamper test. In developer tools, change
vendor_emailto another address you own, then submit. The form should show its error message, only the admin copy should arrive, and nothing should reach the edited address. If the edited address gets the email, check the Call Hook name and the order of the actions. - Open a product with no related vendor. The field should be empty. Submit, then check what the visitor sees and that the admin copy arrived.
Limits
- What the check covers. A visitor can only send to addresses your site signed. At worst, they copy another product's signed vendor pair and reach that vendor. Vendor addresses are still visible in the page source, so add spam protection to the form.
- Several related items. What the macro outputs for several rows depends on JetEngine. Set a limit of 1 if your query offers one, and test a product with two related vendors.
- Single template only. The query uses the current post's ID. In a popup, a listing or another page, the current post may not be the product.
- Plugin updates. The snippet relies on the renderer's form ID and the hook names above. Re-run Step 5 after each update.
Troubleshooting
The hidden input shows the macro text
The snippet isn't running for this field. Check that the snippet is active, that the form ID and field names match (the ID changes when you duplicate or import a form), and that the render toggle is on.
The hidden field is empty on every product
The lookup returned no valid address. Check the relation direction and join order against Step 1, and confirm the Comma-separated fields list holds the CCT field name, not its label. Also make sure the product has a related record with a valid address.
Every vendor submission shows an error
The signature didn't match. Make sure vendor_sig has the same macro as vendor_email and that both names match the snippet.
The wrong person gets the email
Compare your join and WHERE columns with the Step 1 table. Make sure the Dynamic Tag returns the current post's ID, and limit rows to your relation if several share the table.
The recipient is right but nothing arrives
Check Form Records first, then your SMTP setup, SPF and DKIM records, and the spam folder. JetFormBuilder's email troubleshooting guide covers delivery problems. For the listing side of a directory, see our tip on front end submission to a contact button.
Once the tamper test fails cleanly and Form Records stores the right address for each product, every inquiry reaches the vendor tied to that product, and the admin copy shows you where it went. If you'd rather have the whole setup built and tested for your site, post a request on BoostMyCroco.
Frequently Asked Questions
Can I put a JetEngine macro directly into Send Email's Custom email address?
Is it safe to use this vendor email method on a public website?
Does this work if the vendor is a WordPress user instead of a CCT item?
Can I use this form in a popup or inside a listing grid?
Why does the vendor never get the inquiry while the admin copy arrives?
This guide answers a question from our community: Jetform con destinatario custom field de CCT realcionado
Sources
- Send Email Post Submit Action 一 JetFormBuilder (jetformbuilder.com)
- Sending Form Submissions to a Related User Email — JetFormBuilder (crocoblock.com)
- [JetFormBuilder] Allow Send Email recipients to use JetEngine Options Page values · Issue #8627 · Crocoblock/suggestions · GitHub (github.com)
- Allows to use JetEngine macros in the Manual Input option of the Hidden Field of JetFormBuilder (gist.github.com)
- How to Troubleshoot Email Sending — JetFormBuilder (jetformbuilder.com)
- How to Notify a Post Author About Post Approval — JetFormBuilder (jetformbuilder.com)


