Field group
A small form asked once per person or item, repeated as many times as needed — two to five team members each with a name and an email, guests on a booking, line items on an order. Use this whenever the request describes the SAME set of details collected several times over; never a numbered run of separate questions ("Member 1 name", "Member 2 name"), which cannot stretch to a team of six or shrink to a team of two.
field_groupA small form asked once per person or item, repeated as many times as needed — two to five team members each with a name and an email, guests on a booking, line items on an order. Use this whenever the request describes the SAME set of details collected several times over; never a numbered run of separate questions ("Member 1 name", "Member 2 name"), which cannot stretch to a team of six or shrink to a team of two.
How it gets answered — extracted from free text by the agent, then re-validated.
Configuration
Fields specific to field_group. The fields every block has — id, ref, title, required, visibility, media, agentHints, prefillParam, identityField — are documented once.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"buttonLabel": {
"type": "string",
"maxLength": 60
},
"type": {
"type": "string",
"const": "field_group"
},
"fields": {
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 6,
"maxLength": 32
},
"key": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,30}$"
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"kind": {
"type": "string",
"enum": [
"short_text",
"long_text",
"email",
"phone",
"url",
"number",
"date",
"single_select",
"yes_no"
]
},
"required": {
"default": false,
"type": "boolean"
},
"placeholder": {
"type": "string",
"maxLength": 200
},
"options": {
"default": [],
"maxItems": 50,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 6,
"maxLength": 32
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"description": {
"type": "string",
"maxLength": 1000
},
"image_key": {
"default": null,
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"score": {
"type": "number"
}
},
"required": [
"id",
"label"
]
}
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
},
"required": [
"id",
"key",
"label",
"kind"
]
}
},
"itemLabel": {
"default": "Entry",
"type": "string",
"minLength": 1,
"maxLength": 60
},
"minEntries": {
"default": 1,
"type": "integer",
"minimum": 1,
"maximum": 20
},
"maxEntries": {
"default": 5,
"type": "integer",
"minimum": 1,
"maximum": 20
}
},
"required": [
"id",
"ref",
"title",
"type",
"fields"
],
"$defs": {
"__schema0": {
"type": "object",
"properties": {
"op": {
"type": "string",
"enum": [
"and",
"or"
]
},
"conditions": {
"default": [],
"type": "array",
"items": {
"type": "object",
"properties": {
"left": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "ref"
},
"ref": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{1,40}$"
}
},
"required": [
"kind",
"ref"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "variable"
},
"name": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,40}$"
}
},
"required": [
"kind",
"name"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "hidden"
},
"name": {
"type": "string",
"pattern": "^[a-zA-Z_][a-zA-Z0-9_.-]{0,60}$"
}
},
"required": [
"kind",
"name"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "literal"
}
},
"required": [
"kind"
]
}
]
},
"op": {
"type": "string",
"enum": [
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"contains",
"not_contains",
"starts_with",
"ends_with",
"matches_regex",
"is_empty",
"is_not_empty",
"is_checked",
"is_not_checked",
"includes",
"not_includes",
"ranked_above",
"ranked_below"
]
},
"value": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"required": [
"left",
"op"
]
}
},
"groups": {
"default": [],
"type": "array",
"items": {
"$ref": "#/$defs/__schema0"
}
}
},
"required": [
"op"
]
}
}
}In a generated draft these arrive as config pairs: fields=<Label:kind|Label:kind> where kind is one of short_text, long_text, email, phone, url, number, date, single_select, yes_no — append * to make a field required, and list a select's choices in brackets (Role:single_select[Lead|Member]); item=<Team member> names one entry; min=<2>, max=<5> bound how many there may be.
What you receive
GET /v1/forms/{id} and every "next question" projects blocks through toPublicBlock. For the example above:
{
"id": "blk_group001",
"ref": "q_team",
"type": "field_group",
"title": "Who is on your team?",
"required": true,
"imageKey": null,
"media": null,
"groupFields": [
{
"key": "name",
"label": "Full name",
"kind": "short_text",
"required": true
},
{
"key": "email",
"label": "Email",
"kind": "email",
"required": true
},
{
"key": "year",
"label": "Year of study",
"kind": "number",
"required": false
}
],
"itemLabel": "Team member",
"minEntries": 2,
"maxEntries": 4
}What you send
One object per entry, keyed by each field's key. Every value is checked by the rules of that field's own kind, so a column can also fail with any code that kind emits — an email column with invalid_email, a bounded number column with too_small. The hint names the entry and the column.
type Answer = Record<string, string | number | boolean>[];curl -X POST https://api.chatform.in/v1/responses/{RESPONSE_ID}/answers \
-H "x-api-key: $CHATFORM_SECRET_KEY" \
-H "content-type: application/json" \
-d '{"ref": "q_team", "value": [{"name":" Maya Iyer ","email":"Maya@Northwind.CO"},{"name":"Rahul Nair","email":"rahul@northwind.co","year":"3"}]}'What gets stored
The value is normalised before it is saved, so what you read back is not always what you sent.
| You send | Stored | |
|---|---|---|
[{"name":" Maya Iyer ","email":"Maya@Northwind.CO"},{"name":"Rahul Nair","email":"rahul@northwind.co","year":"3"}] | [{"name":"Maya Iyer","email":"maya@northwind.co"},{"name":"Rahul Nair","email":"rahul@northwind.co","year":3}] | each field is canonicalized by the validator for its own kind — the same trimming, lowercasing and coercion it would get as a question of its own |
Errors
| Code | When | Message |
|---|---|---|
required | [{"name":"Maya Iyer"},{"name":"Rahul Nair","email":"rahul@northwind.co"}] — a required field, missing in one entry — the hint names which | Team member 1 · Email is missing. |
type | {"name":"Maya Iyer"} — a single entry is still an array of one | Please fill in the team member details. |
too_few | [{"name":"Maya Iyer","email":"maya@northwind.co"}] — this block asks for at least two | Please give at least 2 team members. |
too_many | [{"name":"A","email":"a@x.co"},{"name":"B","email":"b@x.co"},{"name":"C","email":"c@x.co"},{"name":"D","email":"d@x.co"},{"name":"E","email":"e@x.co"}] | You can add up to 4 team members. |
incomplete | — | |
too_long | — | |
invalid_email | [{"name":"Maya Iyer","email":"nope"},{"name":"Rahul Nair","email":"rahul@northwind.co"}] | Team member 1 · Email — that doesn't look like a valid email address. |
Scheduling
Books a slot on a calendar the builder already owns — Cal.com, Calendly, a Meet room. Needs their booking link. If you do not have one, use `date` instead and ask them for a time directly.
Consent
Agreeing to terms, a waiver, a code of conduct. Put the wording in `description`. By default the only answer is yes; add decline=true when a refusal has to be a real answer you can route on — an eligibility gate, a policy someone may decline.