Skip to main content
JSON Schema files define the structure, validation, and UI rendering of FlowApp action inputs. They are compiled into your code at build time.

Schema basics

Every action requires a JSON Schema file with the same base name:
Actions/
├── BookMeetingAction.cs
└── BookMeeting.json        ← Must match action name

Minimal schema

{
  "type": "object",
  "title": "My Action",
  "required": ["fieldName"],
  "properties": {
    "fieldName": {
      "type": "string",
      "title": "Field Label",
      "description": "Help text shown below the field"
    }
  }
}

Build-time schema generation

The FlowAppSchemaGenerator source generator automatically creates the GetInputSchemaJson() method:
1

Naming convention

The generator looks for a JSON file matching the action class name:
IqraGenerators/FlowAppSchemaGenerator.cs:68-72
var className = classSymbol.Name;
var baseName = className.EndsWith("Action")
    ? className.Substring(0, className.Length - 6)
    : className;
// BookMeetingAction → BookMeeting.json
2

Same directory requirement

The JSON file must be in the same directory as the C# file:
IqraGenerators/FlowAppSchemaGenerator.cs:83-84
var jsonDir = Path.GetDirectoryName(f.Path);
return jsonDir == sourceDirectory && jsonName == baseName;
3

Code generation

The generator creates a partial class method:
IqraGenerators/FlowAppSchemaGenerator.cs:102-117
public partial class BookMeetingAction
{
    public string GetInputSchemaJson()
    {
        return Regex.Unescape("""{\"type\":\"object\",\"title\":\"Book a Meeting\",...}""");
    }
}
If the JSON file is not found or is invalid, compilation will fail with an exception.

Field types

String fields

{
  "attendeeName": {
    "type": "string",
    "title": "Attendee Name",
    "description": "Full name of the meeting attendee",
    "default": "Guest",
    "minLength": 2,
    "maxLength": 100
  }
}

Number fields

{
  "duration": {
    "type": "integer",
    "title": "Duration (minutes)",
    "minimum": 15,
    "maximum": 240,
    "default": 30
  },
  "price": {
    "type": "number",
    "title": "Price",
    "minimum": 0,
    "multipleOf": 0.01
  }
}

Boolean fields

{
  "sendReminder": {
    "type": "boolean",
    "title": "Send Email Reminder",
    "default": true
  }
}

Enum fields

{
  "priority": {
    "type": "string",
    "title": "Priority Level",
    "enum": ["low", "medium", "high"],
    "default": "medium"
  }
}

Array fields

{
  "tags": {
    "type": "array",
    "title": "Tags",
    "items": {
      "type": "string"
    },
    "minItems": 1,
    "maxItems": 10
  }
}

Nested objects

{
  "attendee": {
    "type": "object",
    "title": "Attendee Information",
    "required": ["name", "email"],
    "properties": {
      "name": {
        "type": "string",
        "title": "Name"
      },
      "email": {
        "type": "string",
        "title": "Email",
        "format": "email"
      },
      "phone": {
        "type": "string",
        "title": "Phone Number"
      }
    }
  }
}

Dynamic dropdowns with fetchers

Use the custom x-fetcher property to populate fields from a data fetcher:
IqraInfrastructure/Managers/FlowApp/Apps/CalCom/Actions/BookMeeting.json:38-44
{
  "eventTypeId": {
    "type": "integer",
    "title": "Event Type ID",
    "x-fetcher": "GetEventTypesById"
  }
}
The fetcher key must match a registered IFlowDataFetcher:
IqraInfrastructure/Managers/FlowApp/Apps/CalCom/Fetchers/GetEventTypesByIdFetcher.cs:15
public string FetcherKey => "GetEventTypesById";
When the UI renders this field, it calls:
await FlowAppManager.FetchOptionsAsync(
    appKey: "cal_com",
    fetcherKey: "GetEventTypesById",
    context: currentFormState,
    integration: userIntegration
);
The fetcher returns options:
IqraInfrastructure/Managers/FlowApp/Apps/CalCom/Fetchers/GetEventTypesByIdFetcher.cs:38-43
return result?.Data?.Select(e => new DynamicOption
{
    Label = $"{e.Title} ({e.Length}m)",
    Value = e.Id,
    Description = $"ID: {e.Id}"
}).ToList() ?? new();

Context-dependent fetchers

Fetchers receive the current form state via the context parameter:
public async Task<List<DynamicOption>> FetchOptionsAsync(
    BusinessAppIntegrationDecryptedModel? integration, 
    JsonElement context)
{
    // Get username from form
    if (context.TryGetProperty("username", out var usernameElement))
    {
        var username = usernameElement.GetString();
        // Fetch event types only for this user
    }
}
This enables dependent dropdowns:
{
  "properties": {
    "username": {
      "type": "string",
      "title": "Username"
    },
    "eventTypeId": {
      "type": "integer",
      "title": "Event Type",
      "x-fetcher": "GetEventTypesByUsername"
    }
  }
}

Conditional schemas with oneOf

Use oneOf to define mutually exclusive field groups:
IqraInfrastructure/Managers/FlowApp/Apps/CalCom/Actions/BookMeeting.json:34-74
{
  "oneOf": [
    {
      "title": "By Event ID",
      "required": ["eventTypeId"],
      "properties": {
        "eventTypeId": {
          "type": "integer",
          "title": "Event Type ID",
          "x-fetcher": "GetEventTypesById"
        }
      }
    },
    {
      "title": "By User & Slug",
      "required": ["username", "eventTypeSlug"],
      "properties": {
        "username": {
          "type": "string",
          "title": "Username"
        },
        "eventTypeSlug": {
          "type": "string",
          "title": "Event Type Slug"
        }
      }
    },
    {
      "title": "By Team & Slug",
      "required": ["teamSlug", "eventTypeSlug"],
      "properties": {
        "teamSlug": {
          "type": "string",
          "title": "Team Slug"
        },
        "eventTypeSlug": {
          "type": "string",
          "title": "Event Type Slug"
        }
      }
    }
  ]
}
The UI presents a mode selector, showing only the relevant fields.

Handling oneOf in actions

Check which variant was provided:
IqraInfrastructure/Managers/FlowApp/Apps/CalCom/Actions/BookMeetingAction.cs:64-77
if (input.TryGetProperty("eventTypeId", out var id))
{
    request.EventTypeId = id.GetInt32();
}
else if (input.TryGetProperty("teamSlug", out var team))
{
    request.TeamSlug = team.GetString();
    request.EventTypeSlug = input.GetProperty("eventTypeSlug").GetString();
}
else
{
    request.Username = input.GetProperty("username").GetString();
    request.EventTypeSlug = input.GetProperty("eventTypeSlug").GetString();
}

Validation

All inputs are validated at runtime before action execution:
IqraInfrastructure/Managers/FlowApp/FlowAppManager.cs:289-299
var schemaJson = action.GetInputSchemaJson();
var validationResult = await _schemaValidator.ValidateAsync(
    jsonElement,
    schemaJson,
    $"{appKey}_{actionKey}" // Cache key
);

if (!validationResult.Success)
{
    return ActionExecutionResult.Failure("VALIDATION_ERROR", validationResult.Message);
}

Common validation rules

{
  "type": "object",
  "required": ["email", "name"],
  "properties": {
    "email": { "type": "string" },
    "name": { "type": "string" }
  }
}

Scriban template support

All string fields support Scriban templates at runtime:
{
  "attendeeName": {
    "type": "string",
    "title": "Attendee Name"
  }
}
Users can enter:
{{ customer.firstName }} {{ customer.lastName }}
Which resolves before validation:
IqraInfrastructure/Managers/FlowApp/FlowAppManager.cs:274-280
var renderResult = await _scribanService.RenderDictionaryAsync(rawInputs, sessionContext);

if (!renderResult.Success)
{
    return ActionExecutionResult.Failure("TEMPLATE_ERROR", $"Failed to render inputs: {renderResult.Message}");
}
Templates are resolved before schema validation, so the final rendered value must still satisfy the schema.

Schema file location requirements

The schema generator enforces strict file placement:
Actions/
├── BookMeetingAction.cs
└── BookMeeting.json          ← Same directory
If the schema is not found:
IqraGenerators/FlowAppSchemaGenerator.cs:87-90
if (jsonFile == null)
{
    throw new Exception($"JSON file '{baseName}.json' not found in the same directory as '{sourceFilePath}'");
}

Best practices

{
  "attendeeEmail": {
    "type": "string",
    "title": "Attendee Email",
    "description": "The email address will receive a calendar invite and reminders",
    "format": "email"
  }
}
Descriptions appear as tooltips in the UI.
{
  "timeZone": {
    "type": "string",
    "title": "Time Zone",
    "default": "UTC"
  },
  "sendReminder": {
    "type": "boolean",
    "title": "Send Reminder",
    "default": true
  }
}
Defaults reduce friction for common use cases.
Leverage built-in JSON Schema formats:
{
  "email": { "type": "string", "format": "email" },
  "website": { "type": "string", "format": "uri" },
  "startDate": { "type": "string", "format": "date-time" }
}
Flat schemas are easier for AI to populate:
// ✅ Good - Flat structure
{
  "attendeeName": { "type": "string" },
  "attendeeEmail": { "type": "string" }
}

// ❌ Avoid unnecessary nesting
{
  "attendee": {
    "type": "object",
    "properties": {
      "name": { "type": "string" },
      "email": { "type": "string" }
    }
  }
}
Use nesting only when logically required.
{
  "oneOf": [
    {
      "title": "Book by Event ID (Recommended)",
      "description": "Use when you have a specific event type ID",
      "required": ["eventTypeId"],
      "properties": { ... }
    },
    {
      "title": "Book by Username (Legacy)",
      "description": "Use for personal event types",
      "required": ["username", "eventTypeSlug"],
      "properties": { ... }
    }
  ]
}

Troubleshooting

Ensure:
  1. JSON file is in the same directory as the action class
  2. File name matches the action name (minus “Action” suffix)
  3. JSON file is included in the project with AdditionalFiles build action
Check:
  1. Schema matches the data structure your code expects
  2. Required fields are marked correctly
  3. Data types match (string vs integer, etc.)
  4. Scriban templates resolve to valid values
Verify:
  1. FetcherKey in the fetcher class matches x-fetcher in schema
  2. Fetcher is registered in the app’s DataFetchers list
  3. Integration credentials are valid
  4. Fetcher is not throwing an exception (check logs)

Next steps

Examples

Explore complete FlowApp implementations with advanced schemas

Build docs developers (and LLMs) love