Sequential ID Generator

Prev Next
This content is currently unavailable in Italian. You are viewing the default (English) version.

To download the Sequential ID Generator, visit: Library

In this article, one of our library developers, Tamás, walks through the development decisions behind the Sequential ID Generator building block app available in the library.

Why I built it this way

I wanted this logic block to solve a very practical problem: generate IDs in a way that is reliable, easy to reuse, and fast to deploy in many apps.

The most important design goal was not just “generate the next number.” The real goal was to create something that, once defined, can be copied and reused with very little effort. If a button is built once in a clean way, then it should be possible to place it into another app or another step and only adjust the configuration. I did not want every app builder to reopen the logic and rewrite triggers every time.

This is why the logic block is designed to prioritize configuration over logic. When you use this logic block, you mostly need to think about:

  • what prefix is needed
  • how long the numeric part should be
  • from which table should the button read

Once you define the prefix, number format, and table, the logic blocks should already handle the rest: reading the latest ID, validating it, generating the next value, and checking whether the number still fits the configured length.

The table itself should be the source of truth. A separate counter(which can be a variable as an example) can drift away from the real data if records are inserted, deleted, or edited in unexpected ways. Reading from the table keeps the numbering tied to what actually exists.

Another important decision was to keep the core calculation logic in one place. I did not want one trigger branch for the first ID, another branch for the next ID, and then extra small trigger fragments that later become hard to follow. I wanted the main logic to live in one expression so that the behavior stays consistent and anyone modifying it later knows exactly where to look.

So the logic block is really built around a few practical decisions:

  • easy to copy and reuse
  • safe enough to protect the table from bad ID patterns
  • simple to configure
  • centralized logic so maintenance stays manageable

What the ID looks like

Each generated ID has two parts:
prefix + numeric sequence

Examples:

  • ORD00000125
  • MAT00000003
  • TEST00004567

The prefix tells us what kind of record we are looking at. The numeric part is the running sequence.
I chose to keep the numeric format configuration very simple. Instead of asking the user for a separate number like “8 digits,” the logic block uses a zero pattern such as:

  • 00000000

This is easier to read at a glance. If someone sees eight zeros, they immediately understand that the number section should always be eight characters long. That is why I kept idNumberFormat in this form instead of replacing it with a pure length field.

Examples:

  • 00000000 → 8-digit number
  • 0000 → 4-digit number

That decision is small, but it matters. It makes the logic block more approachable for people who do not want to think in technical parameter terms.

How the build is structured

The actual implementation uses three triggers.
That structure is intentional.
I wanted the logic block to be easy to follow from top to bottom, while still keeping the main logic separate and reusable. So instead of spreading the logic across many editable triggers, I organized it into three stages:
1. USER CONFIG: INPUT STAGE
2. MAGIC
3. USER CONFIG: OUTPUT STAGE

This gives a good balance between usability and control.

  • The first trigger is where you define the inputs.
  • The second trigger contains the internal logic that normally does not need to be edited for the logic block to work.
  • The third trigger is where you decide what to do with the generated result.

This means you normally do not need to work inside the core logic trigger. They only need to configure the input side and the output side.
That was one of the key design decisions: make the logic block reusable by exposing only the parts that usually need to change.

Trigger 1 – USER CONFIG: INPUT STAGE

This is the first trigger you interact with.
Its purpose is to define the input values the logic block needs before any ID can be generated.
In practice, this trigger is used to define:

  • the prefix
  • the idNumberFormat
  • the table ID passed into the connector
    Example:
    prefix = ORD
    idNumberFormat = 00000000
    table ID = target table

This stage is intentionally editable because these are the values that normally change from use case to use case.

You can take the same button, copy it into another app or step, update these configuration values, and reuse the logic block without touching the internal logic.

This supports faster implementation because once the input values are defined, the rest of the flow works automatically.
This trigger also runs the connector call that reads the latest matching record from the selected table.

Why the connector is part of the design

The connector is not just a technical detail. It is part of the reuse strategy.
I use the connector to read the latest existing ID from the target table. The connector receives the table ID as input, applies the prefix filter, sorts the records by _sequenceNumber in descending order, and returns the latest matching record.
This is important for two reasons.
First, it allows the logic block to continue numbering from the real state of the table.
Second, it makes the logic block much easier to reuse. If you copy the button into another app, you do not need to rewrite the logic. You only need to point the connector to the correct table and provide the needed configuration.
This is the practical pattern behind the build:

  1. create the button once
  2. set the table input in the connector
  3. define the prefix and number format
  4. reuse it wherever needed

That was a deliberate choice to support faster app building.

What does the connector do in the logic block?

What happens here:

  • the connector receives the table ID
  • the connector receives the prefix
  • the connector filters records by prefix
  • the connector sorts records by _sequenceNumber in descending order
  • the connector returns the latest matching ID

The connector also supports a very important use case: multiple entities in the same table.

If the same table contains records like:
ORD00000011
MAT00000003
then the logic block should not treat them as one shared sequence. Each prefix should continue only from its own latest value.

So the connector filters by prefix first. That means the next values become:
ORD → ORD00000012
MAT → MAT00000004

I chose this model because different entities can live in the same table, but that does not mean they should share the same running number.

One setup requirement is also worth stating clearly: the connector needs authentication, so a token has to be generated and added to the connector authentication settings. I treat this as setup work, not as part of the logic block’s logic itself, but it is still an important part of making the logic block usable.

I kept this in the input stage because it belongs to setup and context collection. Before the internal logic can work, the logic block first needs to know what it is working with.

Trigger 2 – MAGIC

This is the core logic trigger.
I named it this way on purpose. The idea is simple: this is the part you normally should not need to edit. The logic block should already bring this logic with it.
This trigger takes the configured input values and the latest returned ID from the previous trigger, then performs the internal decision-making.
Even though this is one trigger, several things happen inside it.

1. Format validation - the 'if' part of the trigger

The first check verifies that the latest ID returned from the table still matches the expected structure.
Expected structure:
**prefix + fixed-length number

Example valid value:
ORD00000025
Examples of invalid values:
ORD25
ORD-00025
ABC00000025
ORD00025A

Expression

REGEX_REPLACE(
  LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),
  '^' + Variable._configPrefix + '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$',
  'ok'
)

What this expression does

This expression checks whether the latest ID follows the exact structure the logic block expects.
It does that in three parts:

  • LINK(...) decides which value should be checked. If the connector does not return an ID, the second part is used instead. That second part is the configured start structure, and it allows the logic block to generate the first ID for a new prefix sequence.
  • '^' + Variable._configPrefix means the value must start with the configured prefix.
  • '[0-9]{' + LEN(Variable._configIdNumberFormat) + '}$' means the value must end with exactly the expected number of digits, and nothing extra is allowed after that.

If the whole value matches the expected structure, the result becomes ok. If it does not match, the expression result stays different from ok, and the trigger stops the generation flow and shows an error message

Why I chose this

I added this check because I did not want the logic block to continue from a malformed ID.
If the latest stored value is wrong and the logic accepts it, then the wrong pattern can start propagating into future records. So I wanted the MAGIC trigger’s first responsibility to be stopping that early.
If the format is wrong:

  • _stateOutput is cleared
  • a message is shown to explain why the generation stopped

2. Overflow validation

The second check looks at the next numeric value and verifies that it still fits inside the configured numeric length.

Example:
idNumberFormat = 0000

Allowed range:
0000 to 9999

If the next value would become:
10000
then it no longer fits the configured structure.

Expression

LEN(Variable._configIdNumberFormat)
<
LEN(
  TOTEXT(
    TEXTTOINTEGER(
      REGEX_REPLACE(
        LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),
        '^' + Variable._configPrefix,
        ''
      )
    ) + 1
  )
)

What this expression does

This expression compares two lengths:

  • the allowed numeric length, taken from idNumberFormat
  • the length of the next number after incrementing the current value

The right side works like this:

  • LINK(...) uses the ID returned by the connector when one exists. If the connector does not return an ID, it uses the configured start structure instead. This is what makes first-ID generation possible.
  • REGEX_REPLACE(...) removes the prefix so only the numeric part remains
  • TEXTTOINTEGER(...) + 1 calculates the next numeric value
  • TOTEXT(...) converts that next value back into text
  • LEN(...) measures how many digits that next value needs

If the next number needs more digits than the format allows, the condition becomes true and the trigger stops the process.

Why I chose this

I chose to stop the process here instead of automatically changing the structure. That decision was important because I wanted ID length to stay stable unless someone explicitly changes the configuration.
If overflow is detected:

  • _stateOutput is cleared
  • a message is shown to explain that the maximum length has been reached

3. ID generation

If both checks pass, the trigger generates the next ID.
Example:
ORD00000015 → ORD00000016

This same expression also handles the case where no record exists yet for that prefix.
In that case the logic falls back to the configured starting structure and produces the first value in the sequence.
Example:
ORD00000001

Expression

Variable._configPrefix +
RIGHT(
  Variable._configIdNumberFormat +
  TOTEXT(
    TEXTTOINTEGER(
      REGEX_REPLACE(
        LINK(Variable._stateLastId.id, Variable._configPrefix + Variable._configIdNumberFormat),
        '^' + Variable._configPrefix,
        ''
      )
    ) + 1
  ),
  LEN(Variable._configIdNumberFormat)
)

What this expression does

This is the expression that builds the final ID.
It works in this order:

  • LINK(...) uses the last ID returned by the connector when one exists. If the connector does not return an ID, it uses the configured starting structure instead. This is what allows the same expression to handle the first generated ID as well.
  • REGEX_REPLACE(...) removes the prefix so the numeric part can be processed
  • TEXTTOINTEGER(...)+ 1 increments the number
  • TOTEXT(...) converts the new number back into text
  • Variable._configIdNumberFormat + ... adds the zero pattern in front of the new value
  • RIGHT(..., LEN(Variable._configIdNumberFormat)) keeps only the required number of digits
  • Variable._configPrefix + ... adds the prefix back to create the final ID
    The result is stored in _stateOutput.

Why I chose this

I intentionally kept this as one central expression. I did not want separate internal branches for first ID and next ID if I could avoid it. Keeping the calculation in one place makes reuse easier and future changes safer.
At the end of this trigger, the generated value is stored in _stateOutput.

Trigger 3 – USER CONFIG: OUTPUT STAGE

This is the last trigger in the logic block.
Its purpose is to define what should happen with the generated ID once _stateOutput contains a valid value.
The important point here is that this stage is intentionally left open for you.
If _stateOutput is not blank, then you can decide what the logic block should do next.
In the current setup, the trigger shows a guidance message that explains what to do next:

  • add the configured table to the app
  • replace this message step with a Create Record trigger
  • use _stateOutput as the new record ID

I built it this way because output behavior can vary between use cases.
You may want to create a record immediately. You may want to first store the value in another variable. Or you may want to use it in a larger workflow before writing anything to the table.
So instead of hard-wiring one fixed output behavior, I kept the output stage configurable.
That is why the user-facing part exists on both ends:

  • input can be configured
  • output can be configured
  • the core logic stays in the middle

Why the triggers are separated the way they are

This structure reflects one of the main design choices in the whole logic block: keep responsibilities separate, but keep the core logic centralized.
That is why I did not put everything into one huge trigger.
A single giant trigger might work, but it becomes harder to read, harder to explain, and harder to debug. On the other hand, splitting the actual math into many small pieces also becomes messy.
So I chose a middle path:

  • separate triggers for readable steps
  • one centralized expression for the actual ID calculation

This gives me the benefits of both:

  • the build is easier to follow
  • the logic is easier to maintain
  • reuse is simpler
  • future edits are less risky

Why validation and protection are important in this logic block

This logic block is not only about generating the next number. It is also about protecting the table from bad data.
There are two main protection points:

Format protection

The logic block checks whether the last existing ID matches the expected structure before it continues.
This prevents the sequence from continuing based on IDs that do not match the expected structure.

Overflow protection

The logic block checks whether the next number still fits into the allowed numeric length.
This prevents the structure from changing unexpectedly when the sequence grows.
I considered both of these important because bad ID data can spread quickly. Once an incorrect pattern enters a table and the generator starts using it, that problem can affect every future record.
So the logic block is designed to stop early rather than silently continue in the wrong direction.

Variable naming approach

You may notice that the variable names used inside the logic block follow a specific pattern.
Examples:
_configPrefix
_configIdNumberFormat
_stateLastId
_sequenceNumber

I intentionally used a naming style that is easy to distinguish from variables typically created inside apps.
The purpose is not technical, but practical. When you import or copy the logic block into an app, it should be immediately visible which variables belong to the logic block and which variables belong to the app logic.
Logic block variables use a consistent structure so they are visually grouped together and less likely to conflict with user-defined variables.
This makes the logic block easier to reuse and safer to integrate into existing apps without renaming conflicts or confusion.

Summary of the build decisions

If I reduce the whole logic block to the core decisions behind it, they are these:

  • I wanted fast reuse, so the logic block is configuration-driven.
  • I wanted the table to be the source of truth, so the next ID is based on the latest real record.
  • I wanted the logic to stay maintainable, so the calculation is centralized.
  • I wanted multiple entities in one table to work cleanly, so each prefix has its own sequence.
  • I wanted to protect the table from bad patterns, so format and overflow checks stop bad data before record creation.
  • I wanted the trigger flow to be readable, so the responsibilities are separated into clear steps.

That is why the logic block looks the way it does.
It is not only an ID generator. It is a reusable pattern for controlled ID creation that tries to balance simplicity, reuse, and data protection.

Get Involved

Join the community to share improvements, propose additional units, report issues, or discuss architectural decisions.