This guide explains how to use the custom fields feature in Stood, which allows teams to define additional fields for Account, Contact, Deal, and Activity entities.
The custom fields feature provides a flexible way to extend the data model without code changes. Each team can define their own custom fields by uploading a datamodel.json file to Firebase Storage.
Team-specific Configuration: Each team has its own datamodel.json file stored in Firebase Storage at {teamId}/datamodel.json
Dynamic Loading: The app loads custom field definitions at startup and when switching teams
NoSQL Storage: Custom field values are stored directly in the same Firestore collections as the main entity data
Type Safety: Custom fields support various data types with validation
text: Single-line text input
textarea: Multi-line text input
number: Numeric input with optional min/max validation
boolean: True/false checkbox
date: Date picker
select: Dropdown with predefined options
email: Email input with validation
url: URL input with validation
phone: Phone number input
The datamodel.json file follows this structure:
{
"account": {
"fieldKey": {
"name": "Display Name",
"type": "fieldType",
"required": false,
"defaultValue": "default",
"options": ["option1", "option2"],
"description": "Field description",
"maxLength": 100,
"minValue": 0,
"maxValue": 100
}
},
"contact": { },
"deal": { },
"activity": { }
}name (required): Human-readable field name
type (required): Field type (see supported types above)
required (optional): Whether the field is mandatory (default: false)
defaultValue (optional): Default value for the field
options (optional): Array of options for select fields
description (optional): Help text for the field
maxLength (optional): Maximum length for text fields
minValue (optional): Minimum value for number fields
maxValue (optional): Maximum value for number fields
Custom fields are stored in the same Firestore documents as the main entity data:
{
"name": "Acme Corp",
"description": "Software company",
"customFields": {
"industry": "Technology",
"annualRevenue": 1000000,
"isPublic": true
}
}Each team manages its own custom fields independently
Field definitions are loaded when switching teams
No cross-team field sharing (by design)
Existing entities without custom fields will have an empty customFields map
The system gracefully handles missing or invalid field definitions
No data migration is required
{
"account": {
"industry": {
"name": "Industry",
"type": "select",
"required": false,
"options": [
"Technology",
"Healthcare",
"Finance",
"Manufacturing",
"Retail",
"Education",
"Government",
"Non-profit",
"Other"
],
"description": "The industry sector this account belongs to"
},
"annualRevenue": {
"name": "Annual Revenue",
"type": "number",
"required": false,
"minValue": 0,
"description": "Annual revenue in USD"
},
"employeeCount": {
"name": "Employee Count",
"type": "number",
"required": false,
"minValue": 1,
"description": "Number of employees in the company"
},
"foundedYear": {
"name": "Founded Year",
"type": "number",
"required": false,
"minValue": 1800,
"maxValue": 2024,
"description": "Year the company was founded"
},
"isPublic": {
"name": "Public Company",
"type": "boolean",
"required": false,
"defaultValue": false,
"description": "Whether the company is publicly traded"
},
"notes": {
"name": "Internal Notes",
"type": "textarea",
"required": false,
"maxLength": 1000,
"description": "Internal notes about this account"
}
},
"contact": {
"jobTitle": {
"name": "Job Title",
"type": "text",
"required": false,
"maxLength": 100,
"description": "Professional job title"
},
"department": {
"name": "Department",
"type": "select",
"required": false,
"options": [
"Sales",
"Marketing",
"IT",
"Finance",
"HR",
"Operations",
"Executive",
"Other"
],
"description": "Department within the organization"
},
"seniority": {
"name": "Seniority Level",
"type": "select",
"required": false,
"options": [
"Entry Level",
"Mid Level",
"Senior Level",
"Manager",
"Director",
"VP",
"C-Level"
],
"description": "Seniority level of the contact"
},
"linkedinProfile": {
"name": "LinkedIn Profile",
"type": "url",
"required": false,
"description": "LinkedIn profile URL"
},
"preferredContactMethod": {
"name": "Preferred Contact Method",
"type": "select",
"required": false,
"options": [
"Email",
"Phone",
"LinkedIn",
"SMS",
"Other"
],
"description": "Preferred method of communication"
},
"timezone": {
"name": "Timezone",
"type": "text",
"required": false,
"defaultValue": "UTC",
"description": "Contact's timezone"
},
"lastContactDate": {
"name": "Last Contact Date",
"type": "date",
"required": false,
"description": "Date of last contact with this person"
}
},
"deal": {
"probability": {
"name": "Deal Probability",
"type": "number",
"required": false,
"minValue": 0,
"maxValue": 100,
"description": "Probability of closing this deal (0-100%)"
},
"competitor": {
"name": "Main Competitor",
"type": "text",
"required": false,
"maxLength": 100,
"description": "Primary competitor for this deal"
},
"decisionMaker": {
"name": "Decision Maker",
"type": "text",
"required": false,
"maxLength": 100,
"description": "Name of the key decision maker"
},
"budgetApproved": {
"name": "Budget Approved",
"type": "boolean",
"required": false,
"defaultValue": false,
"description": "Whether budget has been approved"
},
"timeline": {
"name": "Expected Timeline",
"type": "select",
"required": false,
"options": [
"This Quarter",
"Next Quarter",
"This Year",
"Next Year",
"Undecided"
],
"description": "Expected timeline for closing"
},
"source": {
"name": "Deal Source",
"type": "select",
"required": false,
"options": [
"Cold Outreach",
"Referral",
"Website",
"Trade Show",
"Social Media",
"Partner",
"Other"
],
"description": "How this deal was sourced"
},
"customNotes": {
"name": "Deal Notes",
"type": "textarea",
"required": false,
"maxLength": 2000,
"description": "Additional notes about this deal"
}
},
"activity": {
"priority": {
"name": "Priority",
"type": "select",
"required": false,
"options": [
"Low",
"Medium",
"High",
"Urgent"
],
"description": "Priority level of this activity"
},
"estimatedDuration": {
"name": "Estimated Duration (minutes)",
"type": "number",
"required": false,
"minValue": 1,
"maxValue": 480,
"description": "Estimated duration in minutes"
},
"followUpRequired": {
"name": "Follow-up Required",
"type": "boolean",
"required": false,
"defaultValue": false,
"description": "Whether a follow-up is needed"
},
"followUpDate": {
"name": "Follow-up Date",
"type": "date",
"required": false,
"description": "Date for follow-up if required"
},
"outcome": {
"name": "Activity Outcome",
"type": "select",
"required": false,
"options": [
"Successful",
"Needs Follow-up",
"Rescheduled",
"Cancelled",
"No Response"
],
"description": "Outcome of this activity"
},
"location": {
"name": "Location",
"type": "text",
"required": false,
"maxLength": 200,
"description": "Location where activity took place"
},
"attendees": {
"name": "Attendees",
"type": "textarea",
"required": false,
"maxLength": 500,
"description": "List of attendees or participants"
}
}
}