The short answer
A contact form is an online form the CRM hosts and the website embeds. Every form
definition carries a linkId, and the public form lives at a URL built from that
id. The page holds
that URL and an accessible title — no form markup, no field list, no endpoint, no API
key. So the questions change in the CRM without a website deploy.
Then make the submission an upsert: enable createRecordEnabled,
updateRecordEnabled and autolinkEnabled together. Autolink
matches the submitted email against the contact's email field, so a returning applicant
updates their own record instead of becoming a second one. On a live form: 49 submissions,
41 people, nothing unmatched.
The catch is measurement. An embedded form has no recipients, so it has no response rate and no chase list — both have to be rebuilt as reporting over the records it created.
01The business problem
Every website has one: the form behind Contact us, Apply now, Request a call, Register here. It is the same requirement each time, whatever the button says — a stranger types their details on a public page, and the submission has to become a real record straight away. Not a notification somebody transcribes into the CRM, and not a spreadsheet row.
Which in practice means all of this at once:
- the person becomes a record that is owned, typed, and stamped with where they came from;
- somebody submitting twice does not become two people;
- the applicant gets an acknowledgement immediately;
- the right internal person is told;
- the people who started and never finished get chased;
- and marketing can restyle the page, and an administrator can add a question, without either one waiting for the other.
That last constraint is the one that kills most implementations. A form whose field list lives in the website's codebase means every question is a deployment, so the question set freezes at whatever it was on launch day.
02Why the obvious approach fails
Building the form in the website's own stack
The reflex is to hand-build the form in whatever framework the site uses and POST it to the CRM's API. It works on the first day and decays from there. The field list now exists in two places and must be kept in step by hand; adding a question is a code change, a review and a release; and the website has become responsible for validation, for spam, and for holding a credential that can write to the CRM. Every one of those is a problem the hosted form has already solved.
A form product plus an automation bridge
A third-party form tool wired up through an integration platform adds a hop that can fail silently, and it delivers a record with no sub-type, no owner and no provenance, because the bridge does not know your data model. You then need a reconciliation process to finish the job the form should have done.
Copying the form's markup into the page
There is no markup to copy. The hosted form is a JavaScript application served from a static CDN and resolved by its link id at runtime; the HTML at that URL is a shell of about 1.7 KB containing a single custom element. Anything that expects to scrape or re-host the form's fields will find nothing there.
Creating a record per submission
The most expensive mistake is the simplest: turning on record creation and nothing else. People resubmit public forms as a matter of course — they lose the confirmation, they are unsure it worked, they change an answer. On one live application form, 49 submissions came from 41 people. Creation-only would have produced eight duplicate contacts on a form that small, each with its own downstream email, and the deduplication bill arrives later with interest.
Assuming the applicant is a lead
Public form, unknown person — so a lead, surely. Not necessarily. Someone joining a programme is entering a relationship, not a sales pipeline: they have no deal, no value and no close date, and putting them in a pipeline makes every pipeline metric wrong. Bind the form to the entity that matches what the person actually is. The placement question is Blueprint 003.
03Data model
The three entities involved — the form definition, the response, and the relation join — are covered in Blueprint 012. What is specific here is the boundary between the website and the CRM.
The coupling is one opaque string
A form definition exposes linkId, and the public form is served from a URL of the
shape:
https://forms.{vendor-host}/{link-id}/viewform
The website stores that URL and a human-readable title, and nothing else. In a live implementation the page's own content model holds exactly two keys for the form — the URL and the title — and the button opens it in a modal frame at click time. The title is not decoration: it becomes the frame's accessible name, which is the only thing a screen reader has to announce before the frame's contents load.
Why this is the whole point. Because the website holds no field names, adding, removing or reordering a question is a CRM change with no website release. And because it holds no credential, the page cannot be turned into a write path against your CRM by anyone who reads its source.
What embedding actually costs
The hosted form response carries no X-Frame-Options and no
frame-ancestors restriction, which is precisely why it embeds anywhere. Three
consequences follow, and all three are usually discovered late:
- It is not crawlable. The questions are rendered by script, so search engines and models see the shell. Any content that needs to be indexed has to live on the page around the frame, not inside it.
- Without JavaScript there is no form. Not a degraded form — nothing.
- The parent page cannot see inside the frame. Your analytics and consent tooling observe the click that opened it and nothing after, so field-level abandonment is invisible from the outside.
And since nothing restricts who may frame it, the same form can be embedded on a site that is not yours. The link id is the only secret, and it is in your page source.
Form fields are shared across forms
Form field definitions live in a space-wide pool rather than inside one form. Four field identifiers on this application form — first name, last name, email, company — are identical to those on an unrelated customer survey in the same space. So "add an email question" reuses an existing definition rather than making a new one. Whether editing a shared definition propagates to every form using it was not tested; check before renaming one.
What the record needs and the visitor cannot give
A created record still has to satisfy the platform's ordinary requirements, and an anonymous submission satisfies none of them:
| Required | Where it comes from |
|---|---|
| Owner | A fixed id in the creation template. There is nobody to infer it from |
| Sales unit | Likewise fixed. A record with an owner and no unit is rejected |
| Sub-type | A fixed type id, so applicants are distinguishable at record level from everyone else on the same entity — the discriminator pattern in Blueprint 001 |
| Provenance | Stamped by the template — see §04 |
04Field-level configuration
The question set, and what "required" should mean
The live form asks eleven questions, nine of them mandatory:
| Question | Form field type | Required |
|---|---|---|
| First name, Last name | single-line input | yes |
| yes — and it is the autolink key | ||
| Phone | single-line input | yes |
| Company name | single-line input | no |
| Street | text area | yes |
| City, Zip, Country | single-line input | yes |
| State | single-line input | no |
| Privacy & data protection | checkbox | yes |
Two things are worth copying from that table. First, the form field type follows the CRM field type, not the question — the street question is a text area because the underlying contact address field is multi-line, and no configuration on the form changes that. Second, the required set is not the marketing wish list: a full postal address is mandatory because the programme pays people and cannot proceed without it, while company name — the field a marketer would insist on — is optional. Require what the next step genuinely cannot run without, and nothing else.
Note also that every field has prefill switched off. There is no record to prefill from; the prefill-and-autolink combination that identifies a known customer in Blueprint 012 does not apply when the respondent is a stranger.
The three settings that make it an upsert
| Setting | Value | Effect |
|---|---|---|
createRecordEnabled | true | An unmatched submission becomes a new record |
updateRecordEnabled | true | A matched submission updates the existing one |
autolinkEnabled | true | Decides which of the two happens |
autolinkFormFieldId / autolinkRecordFieldId | the email question / the contact email field | The match. These are two separate identifiers in two different id spaces — pairing them is the entire configuration |
linkRecordEnabled | false | Nothing to link to; identity is established by the answer, not by a send |
limitToSingleResponse / checkForExistingResponse | false | Correct here. Repeat submissions are the mechanism, not the problem |
The creation template
With createRecordEnabled, the form carries a record template. It is a
complete record payload, not a patch — it enumerates the standard fields including all
five phone slots and all five email slots, most of them empty:
{
"contactTypeId": "{sub-type-id}",
"ownerId": "{nominated-owner-id}",
"unitId": "{nominated-unit-id}",
"firstName": "<ppl-tag data-id=\"{first-name-field}\" data-relation-type=\"Responses\"></ppl-tag>",
"lastName": "<ppl-tag data-id=\"{last-name-field}\" data-relation-type=\"Responses\"></ppl-tag>",
"email1": "<ppl-tag data-id=\"{email-field}\" data-relation-type=\"Responses\"></ppl-tag>",
"phone1": "<ppl-tag data-id=\"{phone-field}\" data-relation-type=\"Responses\"></ppl-tag>",
"address": "<ppl-tag data-id=\"{street-field}\" data-relation-type=\"Responses\"></ppl-tag>",
"city": "…", "zipCode": "…", "stateProvince": "…", "country": "…",
"email2": "", "email3": "", "phone2": "", "phone3": "",
"middleName": "", "title": "", "position": "",
"accountRelations": [], "tags": [], "staticProfiles": [],
"shareMode": "Standard",
"isUnsubscribed": false,
"comments": "Created via {a hand-typed page path}",
"customFields": {
"cfRegistrationDate": "<ppl-tag data-id=\"CurrentDate\"></ppl-tag>",
"cfSource": "{the page URL}"
}
}
The five decisions inside that payload:
- The sub-type is set at creation. This is what makes "everyone who applied through the website" a filterable set rather than a guess.
- Owner and unit are nominated, not derived. Both are mandatory and neither can come from the visitor. Pick a deliberate placeholder rather than a real person's account, or the placeholder becomes someone's workload by accident.
- Provenance is stamped twice — once in the comment text and once in a dedicated source field. Prefer the field: it is filterable, reportable, and it does not rot. Which brings us to the next point.
- Hand-typed provenance drifts. In the live template the comment string and the source field disagree about the page path, because one of them was typed and never revisited. Nothing validates a free-text string against reality. Keep one source of truth and make it a field.
- The registration date comes from
CurrentDate, not from a process running later.
The consent tick is not on the record. The privacy checkbox is required to submit — and it does not appear anywhere in the creation template. So the consent exists on the response record and inside the response PDF, and is not queryable on the contact. If you ever need to filter, report or prove consent in bulk, map it to a field of its own explicitly. Nothing warns you that a required question went nowhere.
Notification, and the one setting people get backwards
Set notificationEnabled and address it to the form owner, not to the
primary record owner. On a capture form the created record's owner is the nominated
placeholder from the template, so notifying the record owner mails a placeholder. This is the
opposite of the right answer on a survey sent to an existing customer, where the record owner
is the person who needs to know.
The rest, briefly
- reCAPTCHA is a per-form flag. It is off on this form and on for the general contact form in the same space. A public form without it will collect junk, and junk in an upsert form is junk records.
attachResponseAsPdffiles the submission on the record. On anything resembling an application or an agreement, this is what makes the record defensible a year later.- The confirmation page is a promise. Setting it to say "check your email for instructions" creates an obligation that §05 has to honour.
05Automation & logic
The submission handler
One process, bound to this one form by id, triggering on submission. Its trigger entity is the response and its record type is the contact the response resolved to, so the process can read the answers and act on the person in the same run. In the live implementation it is deliberately small — one filter, one email — and it exists to honour the sentence on the confirmation page.
Observed latency: the process last ran about eight seconds after the form's last recorded response. That is one observation, not a benchmark, but it settles the design question: this is a live handoff, not a batch, so the acknowledgement email can be part of the submission experience.
Restoring the metric with a second, sent form
The pattern worth stealing is what happens next. The application form is embedded, so it can never report a rate. The agreement form that follows is sent — and a sent form reports everything. The live funnel, end to end:
| Stage | Measured | What the platform reports |
|---|---|---|
| Embedded application form | 49 submissions → 41 distinct people, 0 unmatched | Response rate: blank. Send count zero |
| Sent agreement form | 144 sends to those 41 people → 35 signed | Response rate: 85.4%, plus a not-responded list |
Two readings fall out of that table. The obvious one: put the measurable stage where the
decision is. The subtler one: sentCount ÷ sentUniqueCount is 144 ÷ 41 ≈ 3.5,
so the ratio between the two send counters tells you how many times you chased each person —
a number nothing else in the platform surfaces.
Chasing the people who did not finish
An embedded form has no not-responded list, because it has no send list. So the chase has to be rebuilt on the far side: a scheduled process over the created records, filtered to those that reached the applied state and never reached the completed one, sending a reminder.
This is the structural lesson of the whole blueprint. Every measurement an embedded form cannot give you has to be reconstructed as reporting over the records it created — which is only possible because §04 insisted on a sub-type, a provenance field and a registration date. Those three fields are what the chase process filters on. Skip them at creation and the follow-up cannot be built at all.
Keeping the family legible
A funnel like this reaches several processes quickly: acknowledge the application, react to the signature, chase the non-signers, score the applicant, run the campaign. They are separate processes because a process carries one filter whose branches are first-match-wins, and these conditions can all be true at once — the constraint and the naming discipline are in Blueprint 011. Bind each one to the specific form ids it serves; a trigger takes a list of forms, so a family of near-identical forms can share a single process rather than multiplying it.
06Limits & trade-offs
An embedded form cannot be measured by the platform. The response rate is responses ÷ unique recipients, and an embedded form has no recipients — so the rate is blank and the sent-and-not-responded list is empty however many people submit. This is arithmetic, not a bug, and no setting changes it.
- A required question can go nowhere. Verified on the live form: the privacy checkbox is mandatory to submit and absent from the creation template, so consent is captured on the response and not on the contact. The same silence applies to any question nobody mapped — the answer is stored, and it is not on the record.
- An upsert keyed on email is a write path keyed on a guessable identifier. Anyone who submits a known address updates that person's record. On a first-contact form the exposure is usually acceptable, because the writable fields are the ones that person owns anyway — but it is a decision, not a default, and it is the same mechanism flagged as a hazard for surveys in Blueprint 012. Where the form updates anything commercially meaningful, match on a value only the intended person could hold.
- The same setting is right in one design and wrong in another. Leaving repeat responses unrestricted is what makes the upsert work here; on a sent survey it is what pushes the response rate over 100%. There is no safe default — decide per form which of the two you are building.
- Required-field count is a conversion cost you cannot see. Nine mandatory fields including a full postal address is a lot to ask a stranger, and abandonment happens inside a frame the parent page cannot observe. You will see the submissions that completed and never the ones that did not, so this trade-off has to be reasoned about rather than measured.
- The form is invisible to crawlers and to visitors without JavaScript, and its content cannot carry any of the page's search or citation weight.
- Nothing restricts who may frame it. No frame-ancestors, no X-Frame-Options — the property that makes embedding trivial also means the form works on any site that knows the link id, and the link id is in your page source.
- Provenance typed as free text drifts, silently and permanently. Two provenance mechanisms on one live form already disagree.
- Ownership is deferred, not solved. The nominated owner satisfies the platform and assigns the work to nobody. A routing process or a shared view is a separate piece of work, and forgetting it is how a capture form ends up feeding a queue no human opens.
- Field definitions are shared space-wide. Identical field identifiers appear on unrelated forms in the same space. Convenient for reuse; the propagation behaviour of an edit was not tested, so treat renaming a shared question as a change to every form that uses it until proven otherwise.
The trade-off worth stating plainly
Hosting the form in the CRM buys the thing that matters most and is hardest to retrofit: the website stops knowing anything about your data model. Questions change without a release, no credential leaves the CRM, and the record arrives typed, owned and stamped. What you give up is everything that depends on seeing the form as part of your page — visual integration beyond the form's own styling, funnel analytics on individual fields, indexable content, and a form that works without JavaScript. For a sign-up form behind a button, that is a good trade. For a form that is the landing page, it is not, and the honest answer there is a hand-built page that posts to the API and accepts the maintenance cost §02 describes.
07Verification
Read on 2026-09-10 from a live production space and the live public page embedding the form — a partner-programme application form in service since April 2026.
- The coupling was verified from both ends. The link id stored on the form definition is byte-identical to the id in the URL held by the public page's content model, which holds exactly two keys for the form: that URL and an accessible title.
-
The public endpoint was fetched. It returns HTTP 200 with a ~1.7 KB HTML shell
containing one custom element and three module scripts from a static CDN, and no
X-Frame-Optionsorframe-ancestorsheader — which is the basis of every embedding claim in §03. - The upsert was confirmed against its own counters: 49 responses, 41 records in the responded category, 0 unknown respondents. Eight submissions therefore matched an existing record instead of creating one, with nothing falling through unmatched.
- Settings and the creation template were read verbatim — the three upsert flags, the autolink pair, the sub-type, the fixed owner and unit ids, both provenance stamps, and the absence of any consent field. The template's enumeration of all five phone and five email slots is what establishes that it is a full payload rather than a patch.
- The field set was read in full: eleven questions, nine required, prefill off on every one, the street question a text area where its siblings are single-line inputs.
- The automation was traced: one process bound to this form id alone, trigger entity the response and record type the contact, two nodes. Its last run timestamp is about eight seconds after the form's last response timestamp.
- The funnel numbers were read from the two forms' own counters, and the 85.4% figure reproduces exactly as 35 ÷ 41 — the same responses-over-unique-recipients arithmetic verified across an estate in Blueprint 012.
Not observed, and stated as such:
- A submission made for the purpose. Every finding is read from stored configuration, stored results and the public endpoint — not from test data pushed through a live form.
- Whether editing a shared form field definition propagates to the other forms using it.
- Whether an empty value in the creation template writes empty or is skipped.
- The reCAPTCHA behaviour itself; only that it is a per-form flag, off on this form and on for another in the same space.
What would signal a regression
- Unknown respondents rising above zero on an upsert form means the autolink pair has broken — and because creation still works, it presents as a healthy form producing records nobody can find.
- The record count growing in step with the response count. On a form with repeat submitters, one record per response means the match has stopped happening and you are accumulating duplicates.
- Responses arriving while the acknowledgement stops. The confirmation page will keep promising an email regardless; the promise and the process are configured in different places and nothing ties them together.
- The nominated owner's record count growing without the chase process running. That is the signature of a capture form feeding a queue nobody works.