<- All posts

How to Build a Policy Management System in 5 Steps

Ronan McQuillan
15 min read Mar 14, 2025

Handling compliance tasks requires us to have a clear record of which policies are in place and how they change over time.

For this, we need a robust system that enables colleagues to interact with our policy data in a structured way. The trouble is the specific internal workflows involved in this are rarely the same from one organization to the next.

Today, we鈥檙e exploring one way of addressing this problem, by building a custom policy management system in 黑料正能量.

By the end, you鈥檒l have a fully working tool that you can easily modify for your own more granular needs in our open-source, low-code platform.

First, though, let鈥檚 start with the basics.

What is a policy management system?

As the name suggests, a policy management system is an internal tool that鈥檚 used to manage our company policies.

In the simplest examples, this might mean enabling CRUD actions, allowing us to create, read, update, or delete policy data.

More often, though, we鈥檒l need to enforce more sophisticated workflows. For instance, creating approval workflows, where certain types of users can request changes to policies while others can respond to or approve these.

We may even require additional layers of approval or more granular permissions for specific users to request or approve changes to particular kinds of policies.

On top of this, policy management tools can be used to provide a record of how our policies have changed over time, providing oversight and auditability.

厂辞鈥

What are we building?

We鈥檙e building a simple policy management solution on top of an existing Postgres database, although with 黑料正能量, we could just as easily use another RDBMS or NoSQL tool of our choice.

This will be built around two user roles. Editors will be able to submit new versions for existing policies, using the policy_versions. Approvers can then approve these, as well as performing full CRUD actions on our policies table.

Along the way, we鈥檒l provide the queries you need to create a look-alike database so you can build along with our tutorial.

When a new policy_version is approved, the relevant policies table will also be updated to reflect these changes.

This means that we can keep our policies up to date with our real-world requirements, while also providing a clear paper trail for how these have evolved over time.

You might also like our guide to building an engineering change management system .

Let鈥檚 get started.

How to build a policy management system in 5 steps

If you haven鈥檛 already, sign up for a 黑料正能量 account, which will allow you to build as many apps as you like for free.

Join 200,000 teams building workflow apps with 黑料正能量

1. Connecting our database

The first thing we鈥檒l need to do is create a new 黑料正能量 app. We could use a pre-built template or an existing application dump, but today we鈥檙e starting from scratch.

When we choose this option, we鈥檒l be prompted to give our app a name and URL extension. We鈥檙e going to go with Policy Management System.

Policy Management System

At this point, we鈥檙e offered a range of data sources that we can connect our app to, including RDBMSs, NoSQL tools, APIs, spreadsheets, and more.

Data

As we said earlier, we鈥檙e using a PostgreSQL database. When we choose this option, we鈥檒l be prompted to enter our connection details.

Config

Then, we鈥檙e offered a choice of which tables we鈥檇 like to Fetch, making them queryable within 黑料正能量. Our database has two tables called policies and policy_versions. We鈥檙e going to select both.

Config

You can use the following query to create our tables:

 1CREATE TABLE policies (
 2
 3  id SERIAL PRIMARY KEY,
 4
 5  title TEXT UNIQUE,
 6
 7  description TEXT,
 8
 9  category TEXT,
10
11  status TEXT CHECK (status IN ('Active', 'Archived')),
12
13  created_at TIMESTAMP DEFAULT NOW(),
14
15  updated_at TIMESTAMP DEFAULT NOW()
16
17);
18
19CREATE TABLE policy_versions (
20
21  id SERIAL PRIMARY KEY,
22
23  policy_id INTEGER REFERENCES policies(id) ON DELETE CASCADE,
24
25  title TEXT,
26
27  description TEXT,
28
29  category TEXT,
30
31  status TEXT CHECK (status IN ('Draft', 'Pending Approval', 'Approved', 'Rejected', 'Archived')),
32
33  created_at TIMESTAMP DEFAULT NOW(),
34
35  updated_at TIMESTAMP DEFAULT NOW()
36
37);

You can populate these with:

 1-- Insert Active Policy
 2
 3INSERT INTO policies (title, description, category, status)
 4
 5VALUES 
 6
 7  ('IT Security Policy', 'Defines security standards for IT systems', 'Security', 'Active'),
 8
 9  ('Employee Conduct Policy', 'Guidelines for employee behavior and ethics', 'Human Resources', 'Archived'),
10
11  ('Data Privacy Policy', 'Outlines data handling and privacy practices', 'Compliance', 'Active');
12
13-- Insert Draft Policy Versions
14
15INSERT INTO policy_versions (policy_id, title, description, category, status)
16
17VALUES 
18
19  (1, 'IT Security Policy', 'Initial draft of IT security policy', 'Security', 'Draft'),
20
21  (2, 'Employee Conduct Policy', 'Initial draft of employee conduct policy', 'Human Resources', 'Draft'),
22
23  (3, 'Data Privacy Policy', 'Initial draft of data privacy policy', 'Compliance', 'Draft');
24
25-- Insert Pending Approval Policy Versions
26
27INSERT INTO policy_versions (policy_id, title, description, category, status)
28
29VALUES 
30
31  (1, 'IT Security Policy', 'Reviewed IT security policy draft', 'Security', 'Pending Approval'),
32
33  (2, 'Employee Conduct Policy', 'Updated draft of employee conduct policy', 'Human Resources', 'Pending Approval');
34
35-- Insert Approved Policy Versions
36
37INSERT INTO policy_versions (policy_id, title, description, category, status)
38
39VALUES 
40
41  (1, 'IT Security Policy', 'Final version of IT security policy', 'Security', 'Approved'),
42
43  (3, 'Data Privacy Policy', 'Final version of data privacy policy', 'Compliance', 'Approved');
44
45-- Insert Rejected Policy Versions
46
47INSERT INTO policy_versions (policy_id, title, description, category, status)
48
49VALUES 
50
51  (2, 'Employee Conduct Policy', 'Rejected version of employee conduct policy', 'Human Resources', 'Rejected');

Here鈥檚 how our policies table looks in 黑料正能量鈥檚 Data section once we鈥檝e Fetched it.

Database

Altering existing columns

In 黑料正能量, we can make adjustments within the Data section that will then be reflected when we start generating UIs and automations.

Firstly, all of our TEXT columns are handled the same way in our database, but 黑料正能量 distinguishes between a few types of textual data. Each database table has a field called description.

We鈥檒l change these from Text to Long Form Text. Remember to repeat this process on both tables.

Long Form

Next, our tables have a few attributes that we only want to offer defined options for. These are the status and category columns on each table. Our possible categories will be Security, Human Resources, and Compliance, although you could add more to suit your needs.

Options

We鈥檒l change the type for these columns to Single Select and input these options. Again, remember to do this for both tables.

The status in each of our tables will have different options. On the policies table, these will be Active and Archived.

Options

For policy_versions, we鈥檒l use Draft, Pending Approval, Approved, and Rejected.

Options

Default values

We can also add Default Values within the Data layer. These will be used as a fallback when a row is created if a value isn鈥檛 specified for a particular column. We鈥檙e going to add these across all of our statuses and dates on both tables.

We鈥檒l start with the statusattributes, using the options picker in their settings. On policies, we鈥檒l default to Active.

Policy Mangement System

We鈥檒l repeat this same process to set the default status for our policy_versions table to Pending Approval.

Next, each of our tables contains columns called created_at and updated_at. We want both of these to be populated with the current date and time when a row is created, so we鈥檒l bind their default values to {{ Date }}.

Date

Again, make sure to repeat this process across both date columns in each of our tables.

Configuring relationships and user columns

Our tables already have the data we need to denote relationships. Each one has a unique id attribute. The policy_id attribute under policy_versions then corresponds to the id attribute in the policies table.

We just need to configure this in 黑料正能量.

Start by hitting Define Relationship.

Relationship

We鈥檙e then presented with this modal, where we can set up our relationship.

Relationship

We鈥檙e going to set this so that one row in the policies table links to many rows on policy_versions, using id as our primary key and policy_id as our foreign key.

Relationship

Now, we can see related rows across each of our tables.

Rows

We also want the ability to link rows on our policy_versions table to specific users, but this works a little bit differently since we鈥檙e dealing with 黑料正能量鈥檚 internal Users table.

We offer two special data types for this, depending on whether we want to relate rows to one user or many.

We鈥檙e going to add two Single User columns to our policy_versions table. The first will be called editor. We鈥檙e also enabling the option to default to current user.

Approver

We鈥檒l then add a second Single User column and call it approver. This time, we don鈥檛 need a default value.

Approver

Adding user roles

Before we go any further, we鈥檒l need to set up our policy management system鈥檚 access roles. As we outlined earlier, we鈥檒l have two roles called editor and approver, who can submit and approve new policy versions, respectively.

We鈥檒l start by hitting Manage Roles. This opens 黑料正能量鈥檚 visual RBAC editor.

RBAC

Here, we can configure our user roles. We can then assign permissions to these elsewhere in the builder.

We鈥檒l start by adding our two roles, editor and approver.

Roles

We want our approvers to inherit their permissions from editors. To do this, we鈥檒l place editorto the left ofapprover` and draw a line between them to denote inheritance.

Hierarchy

Building role-specific views

We can define what permissions we鈥檒l give to users with each role without leaving the Data section.

That is, we鈥檙e going to create database views based on each table that will define which read and write actions each role is granted.

The first thing we need to do is remove their permissions to perform full CRUD actions on the underlying tables. We鈥檒l do this by setting the Access for each table to App Admin, which is the highest user role.

Policy Management System

We鈥檒l start by creating views for our policies table, as the permissions within these will be quite simple. Hit Create a View. We鈥檒l be presented with this modal, where we can give our new view a name.

We鈥檒l call this first one Editor Policies.

Table

Then, we鈥檒l set the Access to Editor.

Access

Then, under Columns, we鈥檒l set everything to read-only.

Permissions

We鈥檒l repeat this process to make a second view called Approver Policies, setting its Access to Approver. This time, we鈥檒l leave all columns readable, except updated_at, which we鈥檒l make read-only.

Permissions

Next, we鈥檒l create role-specific views for our policy_versions table.

We鈥檒l create one called Editor Versions, with the Access set to Editor. This time, we鈥檒l set all columns except title, description, and category to read-only.

View

Finally, Approver Policies will have all columns set to read-only, except for status.

status

And that鈥檚 our data model ready to go. Now, we can start generating automation logic and UIs based on this.

2. Setting up approval logic

Our approval management system will rely heavily on a couple of automation rules. So, we want to build these before we start designing our app screens.

Archiving policies

First, we鈥檙e going to set up a simple rule that will allow Approvers to set the status of policies to Archived with a button press. To do this, we鈥檒l use a Row Action, which we can generate from the Data section.

Head to the Approver Policies view and hit Create row action.

Row Action

When prompted, we鈥檒l call this Archive.

Archive

This creates an automation rule that鈥檚 triggered by user actions on a specific row from the front-end of your app.

Trigger

We鈥檙e going to add a single action step after our trigger. Hit the plus icon. Here, we鈥檒l select Update Row.

Policy Management System

We鈥檒l set the Table to policies and hit the lightning bolt icon to open the bindings drawer for our Row ID.

id

Under Trigger Outputs, we鈥檒l choose id.

Automation

Now, the Update Row action will be taken on whichever row triggered our automation rule.

We鈥檒l use then add the status and updated_at fields.

fields

We鈥檒l set status to Archived as a static value. We鈥檒l then bind the updated_at column to {{ date now "" }}, setting the current timestamp.

date

We can then hit Run Test to confirm that this works.

test

Approving policy change requests

Next, we鈥檙e going to build an automation that will be triggered each time a policy_version is approved. This will take the data from the approved version and use it to update the corresponding row on the policies table.

We鈥檒l start by adding a new automation, which we鈥檒l call Approve. This time, we鈥檙e selecting a Row Updated trigger.

Trigger

We鈥檒l set the Table for our trigger to policy_versions.

table

Next, we鈥檒l add a condition so that we only continue if {{ trigger.row.status }} equals Approved.

Condition

Next, we need to retrieve the relevant policies row that our automation will update. To do this, we鈥檒l add a Query Rows action, pointed at the policies table.

Query Rows

We only want to return the row that鈥檚 related to the policy_versions row that鈥檚 been approved. So, we鈥檒l add a filter condition so that id equals {{ trigger.row.policy_id }}.

Policy Management System

Lastly, we鈥檒l use the id of the row this returns within an Update row action, pointed at the policies table.

binding

We鈥檒l add category, title, updated_at, and description as fields and bind these to their respective values from our trigger output, along with the current timestamp for our updated_at field.

binding

Again, we鈥檒l hit Run Test to confirm that everything works.

Test

3. Building a change request screen

Now that we鈥檝e set up our automation logic, we can start building UIs. Head to the Editor Policies view in the data section and hit Generate App Screen.

Generate

Here, we鈥檒l choose the option for a table with modal forms.

forms

Here鈥檚 how this will look in the Design section.

Table UI

The first thing we鈥檒l do is tidy up our existing UI by editing our Headline component and removing any unnecessary columns from our table.

columns

Editors aren鈥檛 allowed to add new rows to the policies table, so we can also delete our Create New button and the associated modal form.

delete

Here鈥檚 our remaining Edit form.

Form

At present, this updates the policies row that a user clicks on in our table. We鈥檙e going to modify this so that it creates a new policy_versions row, linked to the policies row that a user clicks on.

We鈥檒l start by setting our form鈥檚 Data to Editor Versions and its Type to Create.

Policy Management System

We鈥檒l then remove all fields except policies, title, category, and description.

Fields

We鈥檒l also update our display text, then under Styles, set our Button Position to Top.

Button Position

Now, we don鈥檛 want users to have to specify the policy they鈥檙e editing. Rather, this should be automatically populated based on which table row they click.

Currently, when a user clicks on a row, a State variable is set, using the clicked row鈥檚 _id. We鈥檙e going to set a default value for our policy field using this, with {{ State.ID_5Whzvriuv }}. We鈥檒l then select the option to disable this field, so it can鈥檛 be written.

Default Value

Here鈥檚 how this will look when we preview our app.

Request Edit

However, we also want to display the current values of our policy data, so that users don鈥檛 need to rewrite this from scratch. To do this, we鈥檒l need to expose our form to the entire policies row to which our new version will be related.

We鈥檒l start by nesting our Form Block inside a Data Provider component.

Data Provider

Then, we鈥檒l add a filter to this so that it only returns the single row where _id equals our state variable from before.

Filter

We鈥檒l then add default values to our remaining fields in the format {{ [Policies Data Provider].Rows.0.category }}.

Bindings

Here鈥檚 how this looks in our preview.

Preview

4. Building approver screens

Next, we鈥檒l start building screens for our Approvers.

Policy CRUD UIs

Firstly, we want to build a screen where users with the Approval role will have full CRUD permissions for our policies table.

We鈥檒l start by generating another table UI with modal forms, this time from our Approver Policies view.

App

As before, we鈥檒l start by editing our display text and removing any extraneous columns from our table.

Policy Management System

For our Create form, we鈥檙e removing all columns except title, description, and category. We鈥檝e also moved our button to the top again.

Form

On our Edit form, we鈥檙e leaving all columns visible, but setting the ones we don鈥檛 need to Disabled, we鈥檝e also arranged them into columns using their Layout settings.

Form

Note that when we generated this screen, it automatically included a button to trigger our Archive row action. Since we have this, we don鈥檛 need our Delete button, so we鈥檒l remove that.

Buttons

Lastly, we need to set our updated_at column to the current timestamp whenever a user updates a row. To do this, we鈥檒l open the actions drawer for our Save button.

Here, we can manually specify a value for our column, using {{ date now 鈥溾 }}.

Save Row

Policy version approvals

Next, we鈥檙e going to add a screen where users can review and approve new policy_versions.

So, we鈥檒l start by generating a screen from our Approver Versions view.

Again, we鈥檝e updated our display text and removed any columns we don鈥檛 need.

Table

Approvers don鈥檛 need to create new policy_versions, so we鈥檒l delete the button and modal forms that relate to this. We鈥檝e then replaced our button with an Options Picker with its field and placeholder set to Status.

Search

We鈥檒l set the options for this to match the possible status values from our database, as well as giving it a default value of Pending Approval.

Default Value

Then, we鈥檒l add a filter to our table so that it only returns rows where status equals {{ [Status Filter Options Picker].Value }}

Filter

Now, when we load the screen, only pending requests appear by default.

Preview

Lastly, on our remaining form, we鈥檝e set all fields to disabled except for status.

Policy Management System

Then, under our save button鈥檚 actions, we鈥檒l use the same binding as before to populate an updated_at value, as well as setting our approver column to {{ Current User._id }}.

Current User

From a functional point of view, that鈥檚 our app done.

5. Design tweaks and publishing

Before we push our policy management system live, we鈥檙e going to make a few minor UX improvements.

Firstly, under Screen and Theme, we鈥檒l choose Midnight. While we鈥檙e here, we鈥檒l also adjust our app鈥檚 color scheme to better reflect the 黑料正能量 brand.

Theme

Across each of our tables, we鈥檒l use the Label setting to add proper capitalization to our display texts.

Text

We鈥檒l do the same on our forms using the Label and Placeholder settings.

Nav

Similarly, we鈥檒l use the Label settings under Navigation to make the entries in our nav bar more appropriate.

Nav

Lastly, we鈥檒l remove the navigation entry for our /editor-policies screen since users with the Editor role can only access a single UI.

Nav

Here鈥檚 a reminder of what our finished app looks like.

Turn data into action with 黑料正能量

黑料正能量 is the open-source, low-code platform that empowers IT teams to turn data into action.

To learn more about why organizations of all sizes choose 黑料正能量 to build internal tools, CRUD apps, approval workflows, ticketing systems, and more, head to our features overview.