Appian ACD101 common mistakes
Appian ACD101 · Understand the traps and how to avoid them.
Modeling a one-to-many relationship as a text field on the parent record (for example a comma-delimited list of child IDs) instead of creating a one-to-many relationship between the two record types.
Candidates carry over relational-database habits where the foreign key lives on the child table, so they look for somewhere on the parent to 'store the children' and assume a text column is the only option; they also expect the relationship to be a field they can edit like any other.
What to do instead
Create the relationship on the parent record type, point it at the child record type, and map it to the foreign-key column that already lives on the child data store entity. Query children with the related-record field or a query on the child record type filtered by the parent's ID; never hand-roll a delimited ID list.
Adding or renaming an attribute on the CDT (or in the database) and then expecting record type queries, filters, and sorts to immediately work against it, including filtering on a field whose value is computed or pulled from a related record.
The record type sits on top of a data store entity, and candidates assume it is a live window onto the CDT. They skip the step of defining the field in the record type's data model and re-publishing the data store entity, so the field is unrecognized or unpopulated and filters silently return zero rows.
What to do instead
Add the field explicitly in the record type's data model, map it to the entity column, and publish/deploy the updated data store entity so the schema changes. Treat computed and related-record values as display values: filter and sort on real source columns (or use a query that supports it) and verify with a test query before building interfaces on top.
Building a user input task form that collects values into rule inputs, then leaving the activity's Save Into blank (or saving into a node-local value) and expecting the process variables to be updated after the task completes.
The interface looks correct in preview and the form 'works' from the user's perspective, so candidates assume submission implies persistence. In Appian the task's output only reaches the process when the activity's Save Into maps it to process variables.
What to do instead
On every user input task, open the activity's data tab and set Save Into to assign the form's outputs (or the form's rule inputs) to the process variables. Trace the value forward with a script task or a debug/log step to confirm the process variable is populated after the node.
Creating an exclusive gateway with several conditional paths and no default path, then assuming the process will simply wait or take the last path when no condition evaluates to true.
Candidates think of the gateway as an if/else statement in code where some branch always exists. Without a default, an instance that matches no condition fails or stalls at runtime, which only shows up when edge-case data is tested.
What to do instead
Mark exactly one outgoing path as the default (the unconditional fallback) on every exclusive gateway, and make sure the conditional paths are mutually exclusive and cover all expected data. Test with a record that matches none of the conditions.
Writing saveInto targets in an interface that reference process variables (for example saveInto: pv!customerName) or expecting a local variable to survive after the interface is submitted or refreshed.
Candidates think of the interface as part of the process and assume it can read and write the running instance's variables. Interfaces are evaluated statelessly: they receive rule inputs, hold local variables for that evaluation only, and cannot touch pv!.
What to do instead
Design interfaces to take rule inputs and write only into rule inputs or local variables. Map those rule inputs to process variables in the calling activity's Save Into. If data must persist across steps, push it into process variables or a record, not into local variables.
Placing a submit a!buttonWidget without submit: true and validate: true (or leaving the default validate value), then concluding the form works because the button renders and is clickable.
Candidates focus on layout and appearance, and the component's defaults are permissive, so the button appears to submit while required fields and field-level validations are bypassed and incomplete data reaches the process.
What to do instead
Configure the submit button explicitly with submit: true and validate: true, and test the form by deliberately leaving a required field blank to confirm the submission is blocked. Where possible use a!formLayout's buttons parameter so submission behavior is centralized.
Testing for blank input with an expression such as if(ri!comment = "", ...) or if(ri!comment = null, ...) and expecting a true result when the user left the field empty.
In Appian, any expression involving a null operand evaluates to null rather than true or false, so the branch is never taken and the interface or process silently does the wrong thing. This only surfaces when the field is actually empty, which candidates often skip in testing.
What to do instead
Test emptiness with a!isNullOrEmpty(ri!comment) (or a!isNotNullOrEmpty for the inverse) and supply fallbacks with a!defaultValue(). Build at least one test case for each text, list, and date input where the value is absent, not just populated.
Passing values into an expression rule or interface without matching the declared rule input type - for example feeding text from a form into a rule input typed as Integer - and assuming Appian will convert it automatically.
Candidates equate rule inputs with loosely typed script parameters and don't inspect the rule's input types, so a string like '5' or a single value where a list is expected produces a casting error or a null result at runtime rather than the intended value.
What to do instead
Open the rule's interface and read each rule input's type before calling it; convert explicitly with tointeger(), todecimal(), tostring(), or cast() as needed, and wrap single values in a list where a list is expected. Add a test case in the rule's test panel with the exact types your callers supply.
Assuming that adding a user to a group is enough to let them open a record type, application, or process, and never checking object-level roles or inherited versus explicit security.
Candidates treat groups as the whole permission model. Access is granted by roles (Viewer, Editor, Administrator) assigned to groups on the specific object or an ancestor folder, plus separate security on process models for starting processes, so group membership alone frequently yields 'insufficient privileges'.
What to do instead
For each object a user must reach, verify a role is granted to one of their groups, either explicitly or inherited from a parent folder, and confirm process model security for users who must start a process. Test by logging in as a user who holds only the group membership, not as a designer or administrator.
Expecting an application package to move record data, process instances, or other runtime content into the target environment, and expecting a re-import to create a second copy of an object.
Candidates conflate 'deploying the application' with 'deploying everything about the application'. Packages carry design objects; they do not carry business data or in-flight process instances, and objects are identified by a UUID so a re-import updates the existing object in place.
What to do instead
Inspect the package contents before importing and plan data separately (data store export/import or record data load). After importing, verify object counts and versions in the target environment, and remember that the same UUID is updated rather than duplicated - validate the post-import state rather than assuming a clean overwrite.