create_form
Generate Tally forms with predefined fields and configurations. Convert simple field definitions into Tally's blocks-based structure automatically, with optional status defaulting to DRAFT. Ideal for creating structured forms quickly.
Instructions
Create a new Tally form with specified fields and configuration. This tool converts simple field definitions into Tally's complex blocks-based structure automatically. The status field is optional and defaults to DRAFT if not specified.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| description | No | Optional form description - displayed below the title to provide context | |
| fields | Yes | Array of form fields/questions. Each field will be converted to appropriate Tally blocks automatically. | |
| status | No | Form publication status. Use DRAFT for unpublished forms that are being worked on, or PUBLISHED for live forms. Defaults to DRAFT if not specified. | DRAFT |
| title | Yes | Form title (required) - will be displayed as the main form heading |
Implementation Reference
- src/worker.ts:53-136 (registration)Registration of the 'create_form' tool in the MCP TOOLS array, including full input schema definition with field types, validation, and examples.{ name: 'create_form', description: 'Create a new Tally form with specified fields and configuration. This tool converts simple field definitions into Tally\'s complex blocks-based structure automatically. The status field is optional and defaults to DRAFT if not specified.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Form title (required) - will be displayed as the main form heading', minLength: 1, maxLength: 100 }, description: { type: 'string', description: 'Optional form description - displayed below the title to provide context' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED'], description: 'Form publication status. Use DRAFT for unpublished forms that are being worked on, or PUBLISHED for live forms. Defaults to DRAFT if not specified.', default: 'DRAFT' }, fields: { type: 'array', description: 'Array of form fields/questions. Each field will be converted to appropriate Tally blocks automatically.', minItems: 1, items: { type: 'object', properties: { type: { type: 'string', enum: ['text', 'email', 'number', 'textarea', 'select', 'checkbox', 'radio'], description: 'Field input type. Maps to Tally blocks: text→INPUT_TEXT, email→INPUT_EMAIL, number→INPUT_NUMBER, textarea→TEXTAREA, select→DROPDOWN, checkbox→CHECKBOXES, radio→MULTIPLE_CHOICE' }, label: { type: 'string', description: 'Field label/question text - what the user will see', minLength: 1 }, required: { type: 'boolean', description: 'Whether this field must be filled out before form submission', default: false }, options: { type: 'array', items: { type: 'string' }, description: 'Available options for select, checkbox, or radio field types. Required for select/checkbox/radio fields.' } }, required: ['type', 'label'], additionalProperties: false } } }, required: ['title', 'fields'], additionalProperties: false, examples: [ { title: "Customer Feedback Survey", description: "Help us improve our service", status: "DRAFT", fields: [ { type: "text", label: "What is your name?", required: true }, { type: "email", label: "Email address", required: true }, { type: "select", label: "How would you rate our service?", required: false, options: ["Excellent", "Good", "Fair", "Poor"] } ] } ] } },
- src/worker.ts:57-135 (schema)Detailed JSON Schema for 'create_form' tool input validation, defining supported field types (text, email, etc.), requirements, and examples.type: 'object', properties: { title: { type: 'string', description: 'Form title (required) - will be displayed as the main form heading', minLength: 1, maxLength: 100 }, description: { type: 'string', description: 'Optional form description - displayed below the title to provide context' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED'], description: 'Form publication status. Use DRAFT for unpublished forms that are being worked on, or PUBLISHED for live forms. Defaults to DRAFT if not specified.', default: 'DRAFT' }, fields: { type: 'array', description: 'Array of form fields/questions. Each field will be converted to appropriate Tally blocks automatically.', minItems: 1, items: { type: 'object', properties: { type: { type: 'string', enum: ['text', 'email', 'number', 'textarea', 'select', 'checkbox', 'radio'], description: 'Field input type. Maps to Tally blocks: text→INPUT_TEXT, email→INPUT_EMAIL, number→INPUT_NUMBER, textarea→TEXTAREA, select→DROPDOWN, checkbox→CHECKBOXES, radio→MULTIPLE_CHOICE' }, label: { type: 'string', description: 'Field label/question text - what the user will see', minLength: 1 }, required: { type: 'boolean', description: 'Whether this field must be filled out before form submission', default: false }, options: { type: 'array', items: { type: 'string' }, description: 'Available options for select, checkbox, or radio field types. Required for select/checkbox/radio fields.' } }, required: ['type', 'label'], additionalProperties: false } } }, required: ['title', 'fields'], additionalProperties: false, examples: [ { title: "Customer Feedback Survey", description: "Help us improve our service", status: "DRAFT", fields: [ { type: "text", label: "What is your name?", required: true }, { type: "email", label: "Email address", required: true }, { type: "select", label: "How would you rate our service?", required: false, options: ["Excellent", "Good", "Fair", "Poor"] } ] } ] }
- src/worker.ts:690-742 (handler)Core handler logic for 'create_form': normalizes fields, builds Tally-compatible blocks using utility functions, sends POST to Tally API /forms endpoint.case 'create_form': // Build blocks using BlockBuilder utility for consistency with main codebase const blocks: any[] = []; // Title block (FORM_TITLE) blocks.push(createFormTitleBlock(args.title)); // Optional description block – still TEXT; Tally supports plain TEXT blocks for content sections if (args.description) { blocks.push({ uuid: crypto.randomUUID(), type: 'TEXT', groupUuid: crypto.randomUUID(), groupType: 'TEXT', title: args.description, payload: { text: args.description, html: args.description, }, }); } // Field blocks if (Array.isArray(args.fields)) { args.fields.forEach((field: any) => { const questionConfig = normalizeField(field); createQuestionBlocks(questionConfig).forEach((b) => blocks.push(b)); }); } const payload = { status: args.status || 'DRAFT', // Use provided status or default to DRAFT blocks: blocks }; // Debug logging console.log('=== TALLY API PAYLOAD ==='); console.log(JSON.stringify(payload, null, 2)); console.log('========================'); const createResponse = await globalThis.fetch(`${baseURL}/forms`, { method: 'POST', headers, body: JSON.stringify(payload) }); if (!createResponse.ok) { const errorText = await createResponse.text(); throw new Error(`Tally API error ${createResponse.status}: ${errorText}`); } return await createResponse.json();
- src/utils/block-builder.ts:97-108 (helper)Helper function to create FORM_TITLE Tally block from form title, used in create_form handler.export function createFormTitleBlock(title: string): TallyBlock { return { uuid: randomUUID(), type: 'FORM_TITLE', groupUuid: randomUUID(), groupType: 'TEXT', title, payload: { html: title, }, }; }
- src/utils/block-builder.ts:116-194 (helper)Helper to generate Tally blocks for a single question/field, including title, input, and option blocks. Called in loop for each field in handler.export function createQuestionBlocks(question: QuestionConfig): TallyBlock[] { const blocks: TallyBlock[] = []; const groupUuid = randomUUID(); // Title block for question label blocks.push({ uuid: randomUUID(), type: 'TITLE', groupUuid, groupType: 'QUESTION', title: question.label, payload: { html: question.label, }, }); // Input block const blockType = mapQuestionTypeToBlockType(question.type); const payload: Record<string, any> = { isRequired: question.required ?? false, placeholder: 'placeholder' in question && question.placeholder ? question.placeholder : '', }; if (questionHasOptions(question)) { payload.options = question.options.map((opt) => ({ id: randomUUID(), text: opt.text ?? opt.value ?? opt.id ?? String(opt), })); } // For choice-based questions where Tally expects each option as its own block if (questionHasOptions(question) && ['DROPDOWN', 'MULTIPLE_CHOICE', 'CHECKBOXES'].includes(blockType)) { const optionBlocks: TallyBlock[] = []; const optionGroupUuid = randomUUID(); question.options.forEach((opt, idx) => { let optionType: TallyBlockType | 'DROPDOWN_OPTION' | 'MULTIPLE_CHOICE_OPTION' | 'CHECKBOX'; switch (blockType) { case 'DROPDOWN': optionType = 'DROPDOWN_OPTION'; break; case 'MULTIPLE_CHOICE': optionType = 'MULTIPLE_CHOICE_OPTION'; break; case 'CHECKBOXES': optionType = 'CHECKBOX'; break; default: optionType = 'DROPDOWN_OPTION'; } optionBlocks.push({ uuid: randomUUID(), type: optionType as TallyBlockType, // cast for index signature; runtime value is correct per docs groupUuid: optionGroupUuid, groupType: blockType, title: opt.text ?? opt.value ?? String(opt), payload: { index: idx, text: opt.text ?? opt.value ?? String(opt), }, } as unknown as TallyBlock); }); blocks.push(...optionBlocks); return blocks; } blocks.push({ uuid: randomUUID(), type: blockType, groupUuid, groupType: blockType, title: question.label, payload, }); return blocks; }
- src/worker.ts:1992-2025 (helper)Normalizes simplified input field objects from tool args to full QuestionConfig expected by block-builder utilities.function normalizeField(field: any): import('./models/form-config').QuestionConfig { const { QuestionType } = require('./models/form-config'); const typeMap: Record<string, import('./models/form-config').QuestionType> = { text: QuestionType.TEXT, email: QuestionType.EMAIL, number: QuestionType.NUMBER, select: QuestionType.DROPDOWN, // accept both singular & plural spellings dropdown: QuestionType.DROPDOWN, radio: QuestionType.MULTIPLE_CHOICE, 'multiple_choice': QuestionType.MULTIPLE_CHOICE, checkbox: QuestionType.CHECKBOXES, checkboxes: QuestionType.CHECKBOXES, textarea: QuestionType.TEXTAREA, 'long answer': QuestionType.TEXTAREA, }; const qType = typeMap[(field.type || '').toLowerCase()] ?? QuestionType.TEXT; const qc: any = { id: crypto.randomUUID(), type: qType, label: field.label || 'Untitled', required: field.required ?? false, placeholder: field.placeholder, }; if (field.options) { qc.options = field.options.map((opt: any) => ({ text: opt, value: opt })); } return qc; }