Skip to content

Custom fields

Pierre Lecointre edited this page Sep 24, 2025 · 2 revisions

Custom Fields Guide

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.

Overview

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.

How It Works

  1. Team-specific Configuration: Each team has its own datamodel.json file stored in Firebase Storage at {teamId}/datamodel.json
  2. Dynamic Loading: The app loads custom field definitions at startup and when switching teams
  3. NoSQL Storage: Custom field values are stored directly in the same Firestore collections as the main entity data
  4. Type Safety: Custom fields support various data types with validation

Supported Field Types

  • 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

Configuration File Structure

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": { ... }
}

Field Properties

  • 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

Data Storage

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
  }
}

Team Management

  • Each team manages its own custom fields independently
  • Field definitions are loaded when switching teams
  • No cross-team field sharing (by design)

Migration and Backwards Compatibility

  • 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

Full JSON example

{
  "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"
    }
  }
}

Contact - Stood CRM support & integration

Clone this wiki locally