Functions best practices

Prev Next

Who can use Functions

As of r338, Functions is in Production Early Access. To request access, contact your Customer Success manager.

Read more in the Community thread here.


Use Functions for writing to Tulip tables

Recommended approach

Create functions when multiple apps or app steps write to the same table and you need to enforce:

  • Required fields
  • Valid values (for example, status must be from an approved list)
  • Consistent metadata (created by, created at)

Why this matters

When citizen developers create table records directly in triggers, they may:

  • Not follow ID naming conventions
  • Forget required fields
  • Misspell field values ("in progress" vs. "in-progress")
  • Inconsistently populate metadata

A function enforces the schema and prevents these errors.

Example

Instead of letting each app builder create "Sample" records with inconsistent fields and content, create a Create Sample function that:

  • Requires consistent inputs (job ID, sample type, quantity, timestamp, user, app name)
  • Validates that sample type is from an approved list
Function: CreateSample
Inputs:
  - job_id (text)
  - sample_type (text)
  - quantity (number)
  - app name (text)
  - user requesting (user)

Logic:
1. Validate sample_type is in ["A", "B", "C"]
2. If invalid, return error message
3. Create table record with:
   - ID: generated within function
   - Job ID: input
   - Sample Type: input
   - Quantity: input
   - Lot Number: input (or empty if not provided)
   - Created By: input
4. Return success message

Outputs:
  - status (text): "success" or "error"
  - message (text): error description if failed

When you need to create a table record for a sample, pass inputs to a function. The function handles the rest.

Anti-pattern to avoid

Do not create a function for every single table write operation, even simple ones.

Why this causes problems

  • Creates function sprawl (hundreds of single-use functions)
  • Adds complexity without benefit for simple writes
  • Makes debugging harder (more places to look)

When to avoid functions for table writes

  • Updating an existing table record (for example, updating status or decrementing quantity)
  • Single app writing to a table
  • No required governance or validation

Leverage Functions for repetitive multi-step logic

Recommended approach

If the same sequence of trigger actions occurs on many steps across multiple apps, extract it to a function.

Example: Get ZPL code for printing a label

Function: Get ZPL Code
Inputs:
  - Material ID (text)

Variables:
  - ZPL Code Variable (text)

Outputs:
  - ZPL Code (text)
  
Logic:
1. Update data: Configuration 'ZPL Code Variable' - Set to - [Expression for ZPL Code including the @Inputs.Material ID']
2. Return: 'ZPL Code' Datasource 'ZPL Code Variable'

Anti-pattern to avoid

Do not create functions for logic that only occurs 1-2 times total across all apps.

Why this causes problems

  • Overhead of managing the function exceeds benefit
  • Forces app builders to learn function interface for rare case
  • Makes debugging harder (jump between function and app)

Embed Connector Functions in Functions

Recommended approach

When you use a connector function, pass the connector function's inputs to a function that calls the connector function. The function can then handle timeout retries and error handling in an easier and centralized way, allowing you to simplify app logic across your solution.

Embedding connector functions in functions can also minimize the effort of a potential future ERP migration by reducing the amount of changes needed to be made to individual apps.

Example

Function: Update Process Order
Inputs:
  - Order ID (text)
  - Status (text)

Outputs:
  - Error Message (text)

Variables:
  - HTTP Code (integer)
  - Timeout Retries (integer)
  - Error Message (text)
  
Logic:
1. Run Connector Function
2. If HTTP code is a success, end the function
3. If HTTP code is a timeout and 'Timeout Retries' is less than or equal to a specified value, increment the 'Timeout Retries' and go back to 'Run Connector Function'
4. If HTTP code is a code that cannot be resolved by function logic, return a helpful message in the 'Error Message' output

Pass record IDs, not individual table record fields

Recommended approach

When a function needs data from a table record, pass the record ID and load the record inside the function.

Example

Instead of the following (where ‘Order ID’ is the ID from the Orders table and the other fields are columns in that table):

Function: Process Order
Inputs:
  - Order ID (text)
  - Customer Name (text)
  - Product Id (text)
  - Quantity (number)
  - Due Date (datetime)
  - Priority (text)

Logic:
1. [Perform logic using input fields]

Do this:

Function: Process Order
Inputs:
  - Order ID (text)
  
Logic:
1. Get table record: Orders table where ID = 'Order ID' input
2. [Perform logic using table record fields]

Why this matters

  • Fewer inputs (1 instead of 6+)
  • Function more easily adapts if table schema changes
  • Cleaner function calls in apps
  • Reduces chance of mapping wrong field to wrong input

Name and describe Functions and inputs consistently

Recommended approach

Use a consistent naming convention across all functions in your workspace.

Function names

  • Use verb + noun: Create Sample, Update Job Status, Validate Serial Number
  • Do not use noun only: Sample, Job, Serial Number

Function description

  • Write an intuitive description of what the function does. For example, for a function named "Create Defect", write a description like "Creates a new record in the Defects table with Status OPEN".

Input names

  • Use consistent terminology: always Work Order ID (not sometimes Order ID or Work_id)
  • Match table field names when possible

Logic block names

  • Be explicit: "Get Job Record" not "Get Table Record"
  • Help future readers: "Validate Sample Type" not "Check Value"

Why this matters

  • App builders quickly understand function purpose
  • Reduces cognitive load when using multiple functions
  • Makes "where used" analysis easier
  • Improves documentation and training

Handle input validation and user error messages gracefully

Recommended approach

Always mark function inputs as required fields, and use input validation rules where appropriate. Do not validate inputs in functions.

Why this matters

  • App users get clear feedback when entering values
  • Simplifies function logic by having the app handle input validation and letting the function run with known-valid inputs
  • Prevents corrupt data from bad inputs

Anti-pattern to consider avoiding

Do not validate inputs within the function.

Why this causes problems

  • Redundant with input validation capabilities available in the app editor
  • Additional unnecessary latency of re-checking inputs for validity within the function

Counter-argument to this best practice

App builders can bypass the system by passing blank static values in function inputs. For functions where validated data is critical, it may be best practice to validate inputs in the function itself (in addition to marking required fields in the app).

Minimize table queries inside loops

If your function loops through an array, avoid querying tables on each iteration. Instead, query once before the loop and reference the result.

Use functions for logic, not simple references

Do not create functions that just fetch a single table record—that's what record placeholders are for. Reserve functions for logic that transform data.

Limit output size

If your function returns large objects or arrays, consider returning only the fields the app needs, not the entire object array.


Additional Resources


Did you find what you were looking for?

You can also head to community.tulip.co to post your question or see if others have solved a similar topic!