Create a Module Schema

In this tutorial, we'll guide you through creating module schemas step-by-step. Module schemas define the user interface forms for your workflow actions. This guide focuses on the practical, hands-on

📖 Reference: For complete technical details, see the Module Schema Reference

What You'll Learn

By following this guide, you'll learn to:

  • Create basic schemas with different field types

  • Add dynamic content that loads from your API

  • Implement field validation and conditional logic

  • Test and deploy your schemas

Step 1: Locate Your Schema File

Step 1.1: Navigate to your connector project folder.

Step 1.2: Open the src/modules/ directory.

Step 1.3: Find your action folder (e.g., create_contacts, get_posts).

Step 1.4: Open the version folder (e.g., v1/).

Step 1.5: Locate the schema.json file.

Your file path should look like: src/modules/your_action/v1/schema.json

Step 2: Create Your First Schema

Step 2.1: Open your schema.json file in your code editor.

Step 2.2: Start with this basic structure:

{
  "metadata": {
    "workflows_module_schema_version": "1.0.0"
  },
  "fields": [],
  "ui_options": {}
}

Step 2.3: Save the file.

Step 2.4: Test that your connector loads without errors.

💡 Tip: Always start with the minimal structure and build incrementally.

Step 3: Add Your First Field

Step 3.1: Add a simple text field inside the fields array:

{
  "metadata": {
    "workflows_module_schema_version": "1.0.0"
  },
  "fields": [
    {
      "id": "api_key",
      "type": "string",
      "label": "API Key"
    }
  ]
}

Step 3.2: Save and reload your connector.

Step 3.3: Create a test workflow and add your action.

Step 3.4: Verify the field appears in the form.

📖 Reference: See Field Types for all available field types.

Step 4: Make the Field Required

Step 4.1: Add validation to make the field required:

{
  "id": "api_key",
  "type": "string",
  "label": "API Key",
  "validation": {
    "required": true
  }
}

Step 4.2: Save the file.

Step 4.3: Test the validation by trying to save without entering a value.

Step 4.4: Verify that an error message appears.

📖 Reference: See Validation Rules for all validation options.

Step 5: Add a Password Widget

Step 5.1: Add UI options to hide the API key:

{
  "id": "api_key",
  "type": "string",
  "label": "API Key",
  "validation": {
    "required": true
  },
  "ui_options": {
    "ui_widget": "password"
  }
}

Step 5.2: Save and test that the input is now hidden.

📖 Reference: See UI Widgets for all available widgets.

Step 6: Add a Description

Step 6.1: Add a helpful description for users:

{
  "id": "api_key",
  "type": "string",
  "label": "API Key",
  "description": "Your API key for authentication",
  "validation": {
    "required": true
  },
  "ui_options": {
    "ui_widget": "password"
  }
}

Step 6.2: Save and verify the description appears below the field.

Step 7: Add a Selection Field

Step 7.1: Add a second field for platform selection:

{
  "fields": [
    {
      "id": "api_key",
      "type": "string",
      "label": "API Key",
      "description": "Your API key for authentication",
      "validation": {
        "required": true
      },
      "ui_options": {
        "ui_widget": "password"
      }
    },
    {
      "id": "platform",
      "type": "object",
      "label": "Platform",
      "ui_options": {
        "ui_widget": "SelectWidget"
      }
    }
  ]
}

Step 7.2: Save and verify both fields appear.

Step 8: Add Static Choices

Step 8.1: Add choices to the platform field:

{
  "id": "platform",
  "type": "object",
  "label": "Platform",
  "ui_options": {
    "ui_widget": "SelectWidget"
  },
  "choices": {
    "values": [
      {
        "value": { "id": "linkedin", "label": "LinkedIn" },
        "label": "LinkedIn"
      },
      {
        "value": { "id": "twitter", "label": "Twitter" },
        "label": "Twitter"
      }
    ]
  }
}

Step 8.2: Save and test selecting different platforms.

📖 Reference: See Choices Configuration for choice formats.

Step 9: Set Field Order

Step 9.1: Add the ui_options section at the root level:

{
  "metadata": {
    "workflows_module_schema_version": "1.0.0"
  },
  "fields": [...],
  "ui_options": {
    "ui_order": ["api_key", "platform"]
  }
}

Step 9.2: Save and verify the fields appear in the specified order.

Step 10: Add a Large Text Field

Step 10.1: Add a message field with textarea widget:

{
  "id": "message",
  "type": "string",
  "label": "Message",
  "description": "The message to send",
  "ui_options": {
    "ui_widget": "textarea"
  }
}

Step 10.2: Update the UI order to include the new field:

{
  "ui_options": {
    "ui_order": ["api_key", "platform", "message"]
  }
}

Step 10.3: Save and test the multi-line text area.

Step 11: Add Dynamic Content Setup

Step 11.1: Add a user selection field that will load dynamically:

{
  "id": "user_id",
  "type": "object",
  "label": "User",
  "description": "Select a user",
  "ui_options": {
    "ui_widget": "SelectWidget"
  },
  "choices": {
    "values": []
  },
  "content": {
    "type": ["managed"],
    "content_objects": [
      {
        "id": "users"
      }
    ]
  }
}

Step 11.2: Add it to your UI order:

{
  "ui_options": {
    "ui_order": ["api_key", "platform", "user_id", "message"]
  }
}

Step 11.3: Save the schema.

📖 Reference: See Dynamic Content for content configuration.

Step 12: Implement Dynamic Content in Code

Step 12.1: Open your route.py file in the same folder.

Step 12.2: Find the /content endpoint (or create it if it doesn't exist).

Step 12.3: Add the users content handler:

@router.route("/content", methods=["POST"])
def content():
    try:
        request = Request(flask_request)
        data = request.data

        content_object_names = data.get("content_object_names", [])
        content_objects = []

        for content_name in content_object_names:
            if content_name == "users":
                users = [
                    {"value": {"id": "1", "label": "John Doe"}, "label": "John Doe"},
                    {"value": {"id": "2", "label": "Jane Smith"}, "label": "Jane Smith"}
                ]

                content_objects.append({
                    "content_object_name": "users",
                    "data": users
                })

        return Response(data={"content_objects": content_objects})

    except Exception as e:
        return Response.error(str(e))

Step 12.4: Save the route file.

Step 13: Test Dynamic Content

Step 13.1: Reload your connector.

Step 13.2: Open your test workflow action.

Step 13.3: Click the refresh button next to the User field.

Step 13.4: Verify that the dropdown loads with John Doe and Jane Smith.

Step 13.5: Select a user and save.

Step 14: Add Field Dependencies

Step 14.1: Add a channel field that will affect the user field:

{
  "id": "channel",
  "type": "object",
  "label": "Channel",
  "ui_options": {
    "ui_widget": "SelectWidget"
  },
  "choices": {
    "values": [
      {
        "value": { "id": "general", "label": "General" },
        "label": "General"
      },
      {
        "value": { "id": "tech", "label": "Tech Team" },
        "label": "Tech Team"
      }
    ]
  }
}

Step 14.2: Update the user field to depend on the channel:

{
  "id": "user_id",
  "type": "object",
  "label": "User",
  "description": "Select a user in the channel",
  "ui_options": {
    "ui_widget": "SelectWidget"
  },
  "choices": {
    "values": []
  },
  "content": {
    "type": ["managed"],
    "content_objects": [
      {
        "id": "users_in_channel",
        "content_object_depends_on_fields": [
          {
            "id": "channel"
          }
        ]
      }
    ]
  }
}

Step 14.3: Update UI order:

{
  "ui_options": {
    "ui_order": ["api_key", "platform", "channel", "user_id", "message"]
  }
}

📖 Reference: See Content Dependencies for dependency patterns.

Step 15: Update Content Handler for Dependencies

Step 15.1: Update your content endpoint to handle the dependency:

for content_name in content_object_names:
    if content_name == "users_in_channel":
        form_data = data.get("form_data", {})
        selected_channel = form_data.get("channel", {})
        channel_id = selected_channel.get("id") if selected_channel else None

        if channel_id == "general":
            users = [
                {"value": {"id": "1", "label": "John Doe"}, "label": "John Doe"},
                {"value": {"id": "3", "label": "Alice Brown"}, "label": "Alice Brown"}
            ]
        elif channel_id == "tech":
            users = [
                {"value": {"id": "2", "label": "Jane Smith"}, "label": "Jane Smith"},
                {"value": {"id": "4", "label": "Bob Wilson"}, "label": "Bob Wilson"}
            ]
        else:
            users = []

        content_objects.append({
            "content_object_name": "users_in_channel",
            "data": users
        })

Step 15.2: Save and test that users change based on channel selection.

Step 16: Add Schema Endpoint for Dynamic Updates

Step 16.1: Create a /schema endpoint in your route.py file:

@router.route("/schema", methods=["POST"])
def schema():
    try:
        request = Request(flask_request)
        data = request.data

        # Get the current form data
        form_data = data.get("form_data", {})

        # Load your base schema from the schema.json file
        with open("schema.json", "r") as f:
            base_schema = json.load(f)

        # Apply any dynamic modifications based on form_data
        # For example, modify field visibility or validation

        return Response(data=base_schema)

    except Exception as e:
        return Response.error(str(e))

Step 16.2: This endpoint is called when fields with on_action: {"load_schema": true} are changed.

Step 16.3: The returned schema is merged with the existing schema to update the UI.

Step 17: Test Your Complete Schema

Step 17.1: Save all files.

Step 17.2: Reload your connector in the Stacksync interface.

Step 17.3: Create a new test workflow.

Step 17.4: Add your action to the workflow.

Step 17.5: Test each functionality:

  • Verify all fields appear in correct order

  • Test validation by leaving required fields empty

  • Test dynamic content loading with refresh button

  • Test field dependencies by changing channel selection

Step 18: Handle Data in Execute Endpoint

Step 18.1: Open your route.py file.

Step 18.2: Find the /execute endpoint.

Step 18.3: Add code to handle your schema data:

@router.route("/execute", methods=["POST"])
def execute():
    try:
        request = Request(flask_request)
        data = request.data

        # Get values from your schema
        api_key = data.get("api_key")
        platform = data.get("platform", {})
        channel = data.get("channel", {})
        user_id = data.get("user_id", {})
        message = data.get("message")

        # Use the data for your logic
        result = {
            "success": True,
            "platform": platform.get("label"),
            "user": user_id.get("label"),
            "message": message
        }

        return Response(
            data=result,
            metadata={"processed_at": "2024-01-01T00:00:00Z"}
        )

    except Exception as e:
        return Response.error(str(e))

Step 18.4: Save and test by running your workflow.

Step 19: Add Input Validation

Step 19.1: Add more validation rules to your message field:

{
  "id": "message",
  "type": "string",
  "label": "Message",
  "description": "The message to send",
  "ui_options": {
    "ui_widget": "textarea"
  },
  "validation": {
    "required": true,
    "min_length": 10,
    "max_length": 500
  }
}

Step 19.2: Test that validation works by entering messages that are too short or too long.

Step 20: Final Testing Checklist

Step 20.1: Test all validation rules:

  • Required fields show errors when empty

  • Length limits are enforced

  • Format validation works

Step 20.2: Test dynamic content:

  • Content loads when refreshing

  • Dependencies update correctly

  • No errors in browser console

Step 20.3: Test schema updates:

  • Fields with load_schema: true trigger schema reloads

  • /schema endpoint returns updated schemas correctly

  • UI updates reflect schema changes

Step 20.4: Test workflow execution:

  • Data reaches execute endpoint correctly

  • All field values are accessible

  • Workflow completes successfully

Congratulations!

You've successfully built a complete module schema with:

  • ✅ Basic field types (string, object, number)

  • ✅ Field validation and UI widgets

  • ✅ Dynamic content loading via /content endpoint

  • ✅ Field dependencies

  • ✅ Schema updates via /schema endpoint

  • ✅ Integration with your /execute endpoint

Last updated