get_submissions
Retrieve form submissions from Tally MCP by specifying form ID, with options to filter by date, limit results, or skip entries.
Instructions
Retrieve submissions for a specific form
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| formId | Yes | ID of the form | |
| limit | No | Maximum number of submissions to return | |
| offset | No | Number of submissions to skip | |
| since | No | ISO date string to filter submissions since |
Implementation Reference
- src/server.ts:1446-1458 (registration)Registers the MCP tool 'get_submissions' with name, description, and input schema specifying formId (required), limit, offset, since parameters.name: 'get_submissions', description: 'Retrieve submissions for a specific Tally form', inputSchema: { type: 'object', properties: { formId: { type: 'string', description: 'ID of the form to get submissions for' }, limit: { type: 'number', description: 'Maximum number of submissions to return', minimum: 1, maximum: 100 }, offset: { type: 'number', description: 'Number of submissions to skip for pagination', minimum: 0 }, since: { type: 'string', description: 'Only return submissions created after this ISO 8601 timestamp' } }, required: ['formId'] } },
- src/services/TallyApiClient.ts:409-431 (handler)Core handler function that executes the API call to retrieve form submissions from Tally.so API endpoint `/forms/{formId}/submissions` with support for pagination (page, limit) and status filtering. Handles mock mode and response validation.public async getSubmissions( formId: string, options: { page?: number; limit?: number; status?: 'all' | 'completed' | 'partial'; } = {} ): Promise<TallySubmissionsResponse> { if (this.isMockEnabled()) { const mockRes = await tallyApiMock.getSubmissions(formId, options); return mockRes.data; } const params = new URLSearchParams(); if (options.page) params.append('page', options.page.toString()); if (options.limit) params.append('limit', options.limit.toString()); if (options.status) params.append('status', options.status); const query = params.toString(); const url = `/forms/${formId}/submissions${query ? `?${query}` : ''}`; const response = await this.get(url); return validateTallyResponse(TallySubmissionsResponseSchema, response.data); }
- Helper service method wrapping TallyApiClient to fetch form submissions with SubmissionFilters (status, dates, afterId) and pagination.public async getFormSubmissions( formId: string, options: SubmissionFilters & { page?: number; limit?: number } = {} ): Promise<TallySubmissionsResponse> { const endpoint = `/forms/${formId}/submissions`; const params: Record<string, any> = { page: options.page, limit: options.limit, filter: options.status, startDate: options.startDate, endDate: options.endDate, afterId: options.afterId, }; return this.apiClient.requestWithValidation( 'GET', endpoint, TallySubmissionsResponseSchema, undefined, params, ); }
- Validation schema definition for the 'get_submissions' tool input, matching the registration schema.name: 'get_submissions', description: 'Retrieve submissions for a specific Tally form', inputSchema: { type: 'object', properties: { formId: { type: 'string', description: 'ID of the form to get submissions for' }, limit: { type: 'number', description: 'Maximum number of submissions to return', minimum: 1, maximum: 100 }, offset: { type: 'number', description: 'Number of submissions to skip for pagination', minimum: 0 }, since: { type: 'string', description: 'Only return submissions created after this ISO 8601 timestamp' } }, required: ['formId'] } },
- src/tools/submission-tool.ts:45-356 (helper)Supporting method in SubmissionAnalysisTool to list submissions by calling SubmissionService.getFormSubmissions.public async list(formId: string, filters: SubmissionFilters = {}): Promise<TallySubmission[]> { const response = await this.submissionService.getFormSubmissions(formId, filters); return response.submissions; } public async filterByDateRange(formId: string, startDate: string, endDate: string): Promise<TallySubmission[]> { return this.list(formId, { startDate, endDate }); } public async filterByStatus(formId: string, status: SubmissionStatusFilter): Promise<TallySubmission[]> { return this.list(formId, { status }); } public async formatForAnalysis(formId: string, filters: SubmissionFilters = {}): Promise<FormattedSubmission[]> { const response = await this.submissionService.getFormSubmissions(formId, filters); const questions = response.questions; const submissions = response.submissions; const questionMap = new Map<string, string>(questions.map(q => [q.id, q.title])); return submissions.map(sub => ({ submissionId: sub.id, submittedAt: sub.submittedAt, isCompleted: sub.isCompleted, responses: sub.responses.map(res => ({ questionTitle: questionMap.get(res.questionId) || 'Unknown Question', answer: res.value, })), })); } public async getAverageRating(formId: string, questionTitle: string, filters: SubmissionFilters = {}): Promise<number | null> { const formattedSubmissions = await this.formatForAnalysis(formId, filters); let totalRating = 0; let ratingCount = 0; for (const sub of formattedSubmissions) { for (const res of sub.responses) { if (res.questionTitle === questionTitle && typeof res.answer === 'number') { totalRating += res.answer; ratingCount++; } } } return ratingCount > 0 ? totalRating / ratingCount : null; } public async getResponseDistribution(formId: string, questionTitle: string, filters: SubmissionFilters = {}): Promise<Record<string, number>> { const formattedSubmissions = await this.formatForAnalysis(formId, filters); const distribution: Record<string, number> = {}; for (const sub of formattedSubmissions) { for (const res of sub.responses) { if (res.questionTitle === questionTitle) { const answer = String(res.answer); distribution[answer] = (distribution[answer] || 0) + 1; } } } return distribution; } public async exportToCSV(formId: string, filePath: string, filters: SubmissionFilters = {}): Promise<void> { const formattedSubmissions = await this.formatForAnalysis(formId, filters); if (formattedSubmissions.length === 0) { return; } // Flatten the data for CSV const flattenedData = formattedSubmissions.flatMap(sub => sub.responses.map(res => ({ submissionId: sub.submissionId, submittedAt: sub.submittedAt, isCompleted: sub.isCompleted, questionTitle: res.questionTitle, answer: res.answer })) ); const csv = Papa.unparse(flattenedData); // Ensure the directory exists const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, csv); } public async exportToJSON(formId: string, filePath: string, filters: SubmissionFilters = {}): Promise<void> { const formattedSubmissions = await this.formatForAnalysis(formId, filters); // Ensure the directory exists const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, JSON.stringify(formattedSubmissions, null, 2)); } public async search(formId: string, query: string, filters: SubmissionFilters = {}): Promise<FormattedSubmission[]> { const formattedSubmissions = await this.formatForAnalysis(formId, filters); const lowerCaseQuery = query.toLowerCase(); return formattedSubmissions.filter(sub => sub.responses.some(res => String(res.answer).toLowerCase().includes(lowerCaseQuery) ) ); } }