Apply Property substitution in Custom policy checks
Last updated: July 14, 2025
In Liquibase Pro 4.31+, you can utilize Custom policy checks property substitution when setting the SCRIPT_ARGS during check creation. This enables dynamic and context-aware scripting, such as configuring behavior based on runtime attributes like the current Git branch or target environment. It makes your Custom policy checks more flexible, reusable, and maintainable.
You can substitute any command argument, system property, or environment variable within the Liquibase ecosystem.
Liquibase supports the setting of properties from the following locations, from highest to lowest priority:
1. Liquibase flow file stage parameters 2. Liquibase flow file global parameters 3. Command-line arguments 4. ServletConfig initialization parameters 5. ServletContext initialization parameters 6. Java system properties 7. OS environment variables 8. Configuration data, such as piped standard input, defaults files, and connection profiles and other properties stored in the Liquibase properties file.
For example, command-line arguments override ServletConfig and ServletContext initialization parameters, Java system properties, OS environment variables, and configuration data, while OS environment variables override configuration data only.
SCRIPT_ARGS syntax used when creating a Custom Policy check:
- liquibase.properties file example: targeturl="${liquibase.command.url}" - User supplied property: pipelinename="${PIPELINE_NAME}" - Environment variable example: loglevel="${LIQUIBASE_LOG_LEVEL}"
Procedure
Create a checks settings file
1. Create a checks settings file if you don't already have one. In the CLI, run this command:
liquibase checks show
2. If you don't have a checks settings file, a prompt appears that allows you to create one. Confirm the file creation in the prompt. By default, the settings file is named liquibase.checks-settings.conf
.
Create a new custom policy check
1. Create a new file in your Liquibase working directory or a subdirectory like /scripts
. This file will contain the Python script that is your custom policy check. In this example, we title our new file custom_check_no_tables.py
.
2. Open the new custom_check_no_tables.py
file and add the following custom policy check to it.
#
import Liquibase modules containing useful functions
import liquibase_utilities as lb
import sys
# define reusable variables
obj = lb.get_database_object() # database object to examine
liquibase_status = lb.get_status() # Status object of the check
if loglevel >= 2:
//Notify the user when the loglevel value is triggered
logger.info("LOG LEVEL = " + loglevel)
# write check logic
# this condition checks
if the current object is a table
# and whether it starts with the prefix 'tbl_'
if lb.is_table(obj) and not obj.getName().lower().startswith("tbl_"):
# indicate that the custom check has been triggered
liquibase_status.fired = True
# set the message
for Liquibase to
return when check is triggered
liquibase_status.message = "Tables must start with the prefix tbl_. Add this prefix."
# halt execution of the script
sys.exit(1)
#
default
return code
False
Note: Liquibase will run the check against every object in the database, so this script doesn't need a Python looping mechanism to iterate through database objects.
Configure and customize your new check in the CLI
1. Initiate the customization process.
In the CLI, run this command:
liquibase checks customize --check-name=CustomCheckTemplate
The CLI prompts you to finish configuring your file.
A message displays:
This check cannot be customized directly because one or more fields does not have a default value.
Liquibase will then create a copy of CustomCheckTemplate
and initiate the customization workflow.
2. Give your new check a short name for easier identification. In this example, we will name the check CustomCheckNoTables
Tip: For ease of use, it is a best practice to name your custom check after your Python script.
3. Set the severity to return a code of 0–4 when triggered. In this example, we will set the severity to 1
.
These severity codes allow you to determine if the job moves forward or stops when this check triggers. Learn more here: Use Policy Checks in Automation: Severity and Exit Code.
'INFO' | 0
'MINOR' | 1
'MAJOR' | 2
'CRITICAL' | 3
'BLOCKER' | 4
4. Set SCRIPT_DESCRIPTION
.
In this example, we will set the description to:
This script looks to see if any tables exist and notifies you if one is detected.
5. Set SCRIPT_SCOPE
. In this example, we will set the scope to database
. The Python sample provided in this tutorial requires it.
In general, you should set the scope to changelog
or database
depending on what your custom script does:
c
hangelog
: for example, if your check looks for syntax patterns or attributes in your Liquibase Changelog (the changes you author in your repository). With this value, the check runs once per changeset.d
atabase
: for example, if your check looks for the presence of keys, indexes, or table name patterns in your database schema (including Liquibase Tracking Tables). With this value, the check runs once for each database object.
Tip: It is a best practice for your custom checks to have only one scope, not both scopes.
6. Set SCRIPT_MESSAGE
. This message will display when the check is triggered. In this example we will leave this blank, as we are handling the message in the script.
7. Set SCRIPT_PATH
to the location of your script. You can provide either:
A relative path, which is based on the location of the changelog file you specify with
--changelog-file
. This means that the path is relative to where that changelog file is stored. Example: If your changelog is in a folder calledproject/
, and your script is inproject/scripts/
, the relative path to the script would bescripts/your-script.py
.An absolute path, which starts from the root of the your computer's file system and includes the full path to the script. Example: The full path to the script would be:
/Users/yourname/project/scripts/your-script.py
In this example, we will set the path to:scripts/custom_check_no_tables.py
.
8. Essential step for Property substitution: Set the SCRIPT_ARGS
. This allows you to pass dynamic information into the custom policy check without modifying the Python code. With property substitution, you can reference properties such as environment variables, CLI arguments, or values from the defaults file using standard property substitution syntax. loglevel
is a variable in the Custom policy check and its value is set to the ${LIQUIBASE_LOG_LEVEL}
property which can be changed dynamically in the Environment variables. In this example, we will enter loglevel="${LIQUIBASE_LOG_LEVEL}"
9. Set REQUIRES_SNAPSHOT
. If your script scope is changelog
, set whether the check requires a database snapshot. Specify true
if your check needs to inspect database objects. (If your script scope is database
, Liquibase always takes a snapshot, so this prompt does not appear.)
Note: The larger your database, the more performance impact a snapshot causes. If you cannot run a snapshot due to memory limitations, see Memory Limits of Inspecting Large Schemas.
You have now successfully created and customized a policy check!
Example custom policy check property substitution output
Customization complete. Review the table below to confirm your changes.
+---------------------+-----------+--------+----------+----------+--------------------------------+--------------------------------+
| Short Name | Scope | Type | Status | Severity | Customization | Description |
+---------------------+-----------+--------+----------+----------+--------------------------------+--------------------------------+
| CustomCheckNoTables | changelog | sql, | disabled | 0 | SCRIPT_DESCRIPTION = Custom | Executes a custom check |
| | | xml, | | | check | script. |
| | | yaml, | | | SCRIPT_SCOPE = CHANGELOG | |
| | | json | | | SCRIPT_MESSAGE = The message | |
| | | | | | to display when the check is | |
| | | | | | triggered | |
| | | | | | SCRIPT_TYPE = PYTHON | |
| | | | | | SCRIPT_PATH = null | |
| | | | | | SCRIPT_ARGS = null | |
| | | | | | REQUIRES_SNAPSHOT = false | |
+---------------------+-----------+--------+----------+----------+--------------------------------+--------------------------------+
| MyCustomCheck | changelog | sql, | enabled | 0 | SCRIPT_DESCRIPTION = Custom | Executes a custom check |
| | | xml, | | | check | script. |
| | | yaml, | | | SCRIPT_SCOPE = CHANGELOG | |
| | | json | | | SCRIPT_MESSAGE = The message | |
| | | | | | to display when the check is | |
| | | | | | triggered | |
| | | | | | SCRIPT_TYPE = PYTHON | |
| | | | | | SCRIPT_PATH = python_script.py | |
| | | | | | | |
| | | | | | SCRIPT_ARGS = | |
| | | | | | logfile="${LIQUIBASE_LOG_LEVEL | |
| | | | | | }" | |
| | | | | | REQUIRES_SNAPSHOT = false | |
+---------------------+-----------+--------+----------+----------+--------------------------------+--------------------------------+
Liquibase command 'checks copy' was executed successfully.
Note: If the content of ${property-name}
does not match a property, it is left as-is and it is not removed. The supported format includes alphanumeric characters, +
, -
, .
, and _
.
If a Custom Policy Check calls on a property that is not defined, you will get an error:
Error while executing script 'test.py': Exception: No value found for argument 'test' line: 13