When I first started looking at IDIRA (CyberArk) Terminal Plugin Controller (TPC) plugins, I found the concept a little confusing.
There was a Process.ini, a Prompts.ini, transitions, states, conditions, FAIL(), PowerShell output… and somehow all of these pieces were supposed to work together to perform a password change.
Once I understood one key concept, however, everything started to make much more sense:
A TPC plugin is essentially a state machine.
In this article, I want to explain how I understand TPC plugins now, using a practical scenario: an IDIRA (CyberArk) CPM plugin that launches a PowerShell script to change a user’s password through a REST API.
The goal is not to build the complete plugin here. Instead, I want to focus on the architecture and, especially, on how these components interact:
Process.iniPrompts.ini- PowerShell
- stdout
- API responses
- TPC transitions
ENDFAIL()- CPM error codes
The Scenario
Imagine that we need to manage accounts stored in an application that exposes a REST API.
Our architecture looks roughly like this:
CyberArk CPM | vCyberArk TPC | vPowerShell | vREST API
The PowerShell script performs the actual REST API operations.
TPC, on the other hand, controls the execution of that script.
This distinction is important.
I like to think about the responsibilities this way:
Process.ini"What should I do?"Prompts.ini"What did I receive?"PowerShell"Do the actual work."
Once I started thinking about TPC this way, the relationship between the files became much easier to understand.
First: What Is a State Machine?
Before discussing the files, we need to understand the basic idea of a state machine.
The name sounds more complicated than the concept actually is.
A state machine basically says:
“I am currently at this step. If something happens, move to another step.”
Think about a washing machine:
OFF | vFILLING WATER | vWASHING | vRINSING | vSPINNING | vFINISHED
Each step is a state.
The movement from one state to another is a transition.
For example:
Current state:WASHINGCondition:Washing time finishedNext state:RINSING
TPC follows a very similar concept.
A simplified password-change workflow could look like this:
Start PowerShell | vGet Current Password | vGet New Password | vCall API | +----------+ | | Success Error | | v v END FAIL
This is our state machine.
The Role of Process.ini
The Process.ini file defines the workflow of the plugin.
Two particularly important sections are:
[states]
and:
[transitions]
A state represents something TPC can execute.
For example:
[states]StartScript=(spawn)PowerShell.exe -File OneIdentity.ps1SendCurrentPassword=<pmpass>SendNewPassword=<pmnewpass>END=
The first state starts PowerShell.
The next states can send information to the running process.
Eventually, the plugin reaches either a successful state such as END or an error state.
Starting PowerShell from TPC
One of the things I initially wanted to understand was:
Who actually starts the PowerShell script?
The answer is the Process.ini.
For example:
StartScript=(spawn)PowerShell.exe -File OneIdentity.ps1
The (spawn) built-in action tells TPC to launch the process.
The architecture therefore becomes:
CPM | vTPC | | Process.ini | | (spawn) vPowerShell.exe | vOneIdentity.ps1
This also means that Prompts.ini does not launch PowerShell.
That file has a completely different job.
The Role of Prompts.ini
If Process.ini tells TPC what to do, Prompts.ini helps TPC understand what it received.
Suppose our PowerShell script writes:
Write-Output "CA_REQUEST_CURRENT_PASSWORD"
TPC receives this through the PowerShell output.
We can define a condition in Prompts.ini:
[conditions]GetCurrentPassword=CA_REQUEST_CURRENT_PASSWORD
We have effectively told TPC:
“If you see
CA_REQUEST_CURRENT_PASSWORD, consider the conditionGetCurrentPasswordto have occurred.”
That condition can now be used by the state machine.
Understanding TPC Transitions
This was probably the part that made everything click for me.
Inside Process.ini, we can have:
[transitions]StartScript,GetCurrentPassword,SendCurrentPassword
The format is:
CurrentState,Condition,NextState
In our example:
StartScript,GetCurrentPassword,SendCurrentPassword | | | | | | Process.ini Prompts.ini Process.ini
We can read this almost like an English sentence:
“While I am in
StartScript, if I receive theGetCurrentPasswordcondition, move toSendCurrentPassword.”
That is the state machine in action.
How PowerShell and TPC Communicate
This is where TPC becomes particularly interesting.
The PowerShell script and TPC can effectively have a conversation.
PowerShell can write something to stdout:
Write-Output "CA_REQUEST_CURRENT_PASSWORD"
TPC receives it.
Prompts.ini recognizes it:
GetCurrentPassword=CA_REQUEST_CURRENT_PASSWORD
The transition then tells TPC what to do:
StartScript,GetCurrentPassword,SendCurrentPassword
And the next state might be:
SendCurrentPassword=<pmpass>
The communication looks like this:
PowerShell | | stdout | | CA_REQUEST_CURRENT_PASSWORD vTPC | | Prompts.ini matches it vGetCurrentPassword | | transition vSendCurrentPassword | | <pmpass> vPowerShell
PowerShell can read the value using standard input, for example:
$currentPassword = [Console]::ReadLine()
The same concept can be used for the new password.
Why Not Pass Passwords on the PowerShell Command Line?
It might be tempting to do something like:
PowerShell.exe script.ps1 username CurrentPassword NewPassword
But that is not a good design for credentials.
Instead, TPC can start the PowerShell process and then provide sensitive information through the interaction with the running process.
For example:
TPC starts PowerShell | vPowerShell asks for current password | vTPC sends <pmpass> | vPowerShell asks for new password | vTPC sends <pmnewpass>
The PowerShell script doesn’t need to retrieve those passwords directly from the Vault.
TPC already has access to the relevant CPM variables.
Calling the REST API
Once PowerShell has the information it needs, it can perform the real operation.
Conceptually:
try { Write-Output "DEBUG: Calling password change API" $response = Invoke-RestMethod ` -Uri $ApiUrl ` -Method POST ` -Body $Body Write-Output "CA_API_SUCCESS"}catch { Write-Output "CA_API_ERROR"}
But we can make this much better.
Instead of simply returning a generic error, PowerShell can interpret the API response and translate it into something meaningful to TPC.
Using Control Messages
I found it useful to think of PowerShell output as having two categories.
The first category is control messages.
For example:
CA_API_SUCCESSCA_AUTHENTICATION_FAILEDCA_USER_NOT_FOUNDCA_PASSWORD_POLICY_REJECTEDCA_SERVER_ERRORCA_UNEXPECTED_ERROR
These messages are designed specifically for TPC.
They can be mapped in Prompts.ini:
[conditions]Success=CA_API_SUCCESSAuthenticationFailed=CA_AUTHENTICATION_FAILEDUserNotFound=CA_USER_NOT_FOUNDPasswordPolicyRejected=CA_PASSWORD_POLICY_REJECTEDServerError=CA_SERVER_ERRORUnexpectedError=CA_UNEXPECTED_ERROR
The state machine can then react appropriately.
Don’t Make TPC Understand the Entire API
One design decision I particularly like is keeping API-specific complexity inside PowerShell.
Imagine the API returns:
HTTP 400
with a JSON response indicating:
Password does not meet complexity requirements.
Instead of making Prompts.ini understand the entire JSON response, PowerShell can translate it:
HTTP 400+API response+PowerShell interpretation | vCA_PASSWORD_POLICY_REJECTED
TPC only needs to understand:
CA_PASSWORD_POLICY_REJECTED
This gives us a nice separation:
REST API | | complicated HTTP/JSON response vPowerShell | | interprets the response vSimple CONTROL message | vPrompts.ini | vProcess.ini
PowerShell becomes the translation layer between the external API and TPC.
Not Every PowerShell Output Needs to Be in Prompts.ini
This was another important realization for me.
PowerShell can generate output that is useful for troubleshooting without using that output to control the state machine.
For example:
Write-Output "DEBUG: Starting password change"Write-Output "DEBUG: Target user = $Username"Write-Output "DEBUG: Calling REST API"Write-Output "DEBUG: HTTP Status = $StatusCode"Write-Output "DEBUG: API returned an error"Write-Output "CA_USER_NOT_FOUND"
Only:
CA_USER_NOT_FOUND
needs to be defined in Prompts.ini.
The other messages exist for troubleshooting.
Conceptually:
PowerShell stdout | +--- DEBUG: Calling REST API | | | +--> useful for humans | +--- DEBUG: HTTP Status = 404 | | | +--> useful for humans | +--- CA_USER_NOT_FOUND | +--> Prompts.ini | v State machine
This gives us much better visibility when investigating a plugin failure.
Debug Messages vs Control Messages
I therefore like to keep a clear distinction.
Diagnostic messages:
DEBUG: Authentication request startedDEBUG: Authentication successfulDEBUG: Password change request startedDEBUG: HTTP status = 400DEBUG: API error message = Password policy violation
Control messages:
CA_API_SUCCESSCA_AUTHENTICATION_FAILEDCA_USER_NOT_FOUNDCA_PASSWORD_POLICY_REJECTEDCA_SERVER_ERRORCA_UNEXPECTED_ERROR
Diagnostic messages are primarily for humans.
Control messages are primarily for the TPC state machine.
That distinction makes the plugin much easier to troubleshoot and maintain.
Be Very Careful With Debug Output
There is an important security consideration here.
If PowerShell output can end up in TPC/CPM traces, we should never do this:
Write-Output "Current password = $CurrentPassword"Write-Output "New password = $NewPassword"Write-Output "Bearer token = $Token"
The same applies to blindly logging complete REST request bodies:
Write-Output ($Body | ConvertTo-Json)
If the body contains the new password, we have just written a credential into a log file.
Instead, log the operation:
Write-Output "DEBUG: Current password received from TPC"Write-Output "DEBUG: New password received from TPC"Write-Output "DEBUG: Authentication token obtained"Write-Output "DEBUG: Password change request submitted"Write-Output "DEBUG: HTTP Status = $StatusCode"
Log what happened, not the secrets used to make it happen.
What Does FAIL() Do?
Another important part of Process.ini is the FAIL() built-in action.
For example:
UserNotFound=FAIL(One Identity user was not found,8102)
When the state machine reaches UserNotFound, TPC terminates the plugin execution as a failure.
We can think about the syntax as:
FAIL(ErrorMessage,ErrorCode)
For example:
One Identity user was not found
is the human-readable error, while:
8102
is the plugin return code.
A transition could look like:
CallAPI,UserNotFoundCondition,UserNotFound
The complete flow becomes:
PowerShell | | CA_USER_NOT_FOUND vPrompts.ini | | UserNotFoundCondition vProcess.ini transition | vUserNotFound | vFAIL(One Identity user was not found,8102) | vTPC terminates with failure | vCPM
END vs FAIL()
This also makes the difference between END and FAIL() easy to understand.
Successful path:
API | | success vPowerShell | | CA_API_SUCCESS vPrompts.ini | | Success vProcess.ini | vEND | vSUCCESS
Failure path:
API | | user doesn't exist vPowerShell | | CA_USER_NOT_FOUND vPrompts.ini | | UserNotFound vProcess.ini | vFAIL(...,8102) | vFAILURE
In other words, most paths through our state machine will eventually lead to either:
END
or:
FAIL()
Designing Good Plugin Error Codes
I also wouldn’t simply make the plugin error code equal to the HTTP status.
For example:
HTTP 401 → plugin error 401HTTP 404 → plugin error 404
The HTTP status describes what happened at the REST protocol/API level.
The plugin return code should describe the problem from the CPM/plugin perspective.
For example, we might define:
8101 - Authentication failed8102 - Target user not found8110 - Authorization denied8141 - Password rejected by policy8142 - Password change not permitted8161 - API timeout8162 - Connection failure8163 - TLS/certificate problem8164 - API server error8181 - Invalid API response8199 - Unexpected error
The exact range should be chosen carefully for your environment and checked against CyberArk’s documented/reserved codes, but the important idea is to create a consistent error catalog instead of choosing random numbers.
The architecture then becomes:
HTTP/API response | vPowerShell determines the meaning | vCONTROL MESSAGE | vPrompts.ini condition | vProcess.ini transition | vPlugin error code
For example:
HTTP 400+"Password does not meet policy" | vCA_PASSWORD_POLICY_REJECTED | vPasswordPolicyRejected | vFAIL(New password rejected by password policy,8141)
This is much more useful than simply reporting:
HTTP 400
to whoever is troubleshooting the CPM.
Putting Everything Together
Let’s look at the complete architecture one more time.
CyberArk CPM
|
v
CyberArk TPC
|
Process.ini
|
spawn
|
v
PowerShell
|
"CA_REQUEST_PASSWORD"
|
stdout
|
v
Prompts.ini
|
condition matched
|
v
Process.ini transition
|
Send <pmpass>
>
|
stdin
|
v
PowerShell
|
v
REST API
|
HTTP + JSON
|
v
PowerShell
|
interprets result
|
+-------------+-------------+
| |
CA_API_SUCCESS CA_USER_NOT_FOUND
| |
v v
Prompts.ini Prompts.ini
| |
v v
Process.ini Process.ini
| |
v v
END FAIL(...,8102)
| |
v v
SUCCESS FAILURE
Once I understood this flow, TPC stopped looking like a collection of unrelated INI files and started looking like a relatively simple orchestration engine.
The Mental Model I Use
If I had to summarize everything in a few lines, this is what I would remember:
Process.ini= What should I DO?Prompts.ini= What did I RECEIVE?PowerShell= Do the real work.stdout= PowerShell talks to TPC.stdin= TPC talks to PowerShell.Conditions= Give meaning to received output.Transitions= Decide where the state machine goes next.END= Finish successfully.FAIL()= Finish with an error.
And perhaps the most useful sentence of all:
“I am in this state. If I receive this condition, move to that state.”
That simple sentence explains a surprisingly large part of how a CyberArk TPC plugin works.
Final Thoughts
TPC plugins initially looked more complicated to me than they actually were.
The breakthrough was understanding that Process.ini, Prompts.ini, and PowerShell are not competing with each other. They have different responsibilities.
Process.ini orchestrates the workflow.
Prompts.ini recognizes conditions.
PowerShell handles the application-specific logic — in this case, communicating with a REST API.
Once those responsibilities are clearly separated, it becomes much easier to design the plugin, troubleshoot failures, and add meaningful logging without turning the state machine into a collection of API-specific details.
In a future article, I may take this architecture one step further and build a practical TPC plugin example from beginning to end, including the Process.ini, Prompts.ini, PowerShell script, API error handling, and troubleshooting through TPC traces.
Official Reference
CyberArk documentation:
Terminal Plugin Controller (TPC)
CyberArk — Terminal Plugin Controller

Leave a comment