Written guide
Companion source code
Get the course source code on GitHub. The folder includes the supplied examples and setup instructions. Keep credentials in your own environment; use the maintained API reference when adapting recorded request examples.
A complete connected form → assigned decision → persisted result use case. Actual isolated outcomes verified for approval/rejection, reassignment, escalation, all-reviewer mode and missing-task recovery. External email delivery is disabled in the training environment.
Before you start
Use an editable training organization, one configuration administrator and two ordinary staff reviewers in that organization. Keep separate sessions for each reviewer. Use synthetic submissions and verify your organization's notification settings before decisions. The recorded training organization has delivery disabled; this is not a demonstration of delivered email.
- AppEngine Introduction: account and endpoint
- Staff sign-in and completed session
- Current staff authentication reference
- Workflow, Handling and Approvals APIs
- Workflow definitions and task lifecycle
- Approval decisions and reviewer tray
- CRM and public forms
- Forms course: fields, access and submissions
- Maintained API reference
The maintained API reference is where you find current operation parameters and schemas. Search for workflow, approval, and client/forms. This course explains how those platform capabilities connect; the download retains the complete implementation examples.
1. AppEngine Event-driven Business Workflows
A customer sends a request. The right person receives it, makes a decision, and the saved record shows the result. Your application provides the experience; AppEngine provides the process behind it. We'll build that connection with a real warranty form, a reviewer desk, and the APIs that connect them. Then we'll handle rejection, another reviewer, escalation, and a request that was saved before its workflow could start.

2. A form, a record and a decision
Start with the whole connection. The website submits answers to a named AppEngine form. That form checks its access rule and required fields, then creates a form submission. The answers belong to that record. The workflow definition is different: it describes stages, who receives the work, and what each decision means. One task connects this particular submission to those stages. Its owner points back to the submission. When Priya approves, the approval API checks her assignment, moves the task to Approved, updates the submission's status, and preserves the decision history. Updating a status field yourself would skip that connected operation. We use the existing form and approval actions, and configure the process our application needs.

3. Connect the organization and staff sessions
You need an AppEngine endpoint, your organization ID, and staff accounts in that organization. The linked Introduction and Authentication courses show account setup and where these values come from. This course includes its runnable client and complete requests. Start with the organization's configuration administrator to save the form and definition. Sign in through profile, user, sign in, with the organization header and that person's credentials. Keep the returned token private. Priya and Sam are ordinary staff reviewers: each signs in separately and sends their own bearer token. A public form submission uses the form's access rule; it does not inherit the administrator's token. Our local reference server forwards the organization header and the current caller's token. It never hands the customer an administrator credential.
Start at studio.appmint.io using your organization account. Obtain the organization ID and API endpoint from your deployment documentation; the Studio website is not the API base URL. Follow the linked staff sign-in section and complete any offered verification or password-change challenge before using the returned access token. Sign in separately as configuration administrator and ordinary reviewer. Keep the orgid header attached to every request. Only use a staff token for staff configuration, tray, repository and decision endpoints. The public form does not use that token. An administrator decision does not establish that an ordinary reviewer has access.

POST /profile/user/signin
{
"email": "[email protected]",
"password": "[redacted]"
}POST /profile/user/signin
{
"email": "[email protected]",
"password": "[redacted]",
"token": "[redacted]"
}POST /approval/task/6acb15d0ba781cd23c2f6e53/approve
{
"note": "Attempt by a reviewer not assigned to this task"
}4. Define assignment and terminal outcomes
Create the workflow definition before connecting the form. Give it a stable name, a readable title, and enabled true. The start stage waits for approval. Its assignment rule names Priya's staff email. These are organization users, not customer addresses. The two terminal stages are named Approved and Rejected because the approval service recognizes those decision branches. Each maps the source record to completed, with its own approved or rejected status. The start stage also defines an escalation after two days, adding Sam if review needs help. Save the response ID: a workflow name connects this form, while the workflow ID is useful when explicitly starting an existing record later. The downloaded definition includes every stage and rule, not just the displayed excerpt.
The complete POST /workflow/definition body is downloadable. Name is a stable key; title is a label. enabled:true permits new tasks. Start-stage assignRule.type users resolves the listed organization staff emails. Approved and Rejected are recognized decision branches; type done marks each terminal. modelState and modelStatus map the owner record. notificationTemplate is empty for this controlled example. The escalation uses a two-day threshold and Sam as the additional staff reviewer. Keep the returned definition sk.

POST /workflow/definition
{
"name": "wf-usecase-1011-warranty",
"title": "Warranty Claim Approval",
"enabled": true,
"notificationTemplate": [],
"stages": [
{
"id": 0,
"type": "start",
"name": "Waiting for approval",
"modelState": "inprogress",
"modelStatus": "new",
"assignRule": {
"type": "users",
"users": [
"[email protected]"
],
"fallbackRoles": [
"Owner"
]
},
"notificationTemplate": [],
"escalations": [
{
"escalateAfter": 2,
"units": "days",
"escalateRule": {
"type": "users",
"users": [
"[email protected]"
]
},
"notificationTemplate": []
}
]
},
{
"id": 1,
"type": "done",
"name": "Approved",
"modelState": "completed",
"modelStatus": "approved",
"notificationTemplate": []
},
{
"id": 2,
"type": "done",
"name": "Rejected",
"modelState": "completed",
"modelStatus": "rejected",
"notificationTemplate": []
}
]
}5. Create the form and its schema
The form is a built-in CRM form record. Its name is the URL key your client will request. Its title is what people see. Set it open, choose email access, and set workflow to the definition's name. Our schema asks for the customer's name, email, order number, bike model, and problem. Required belongs in the schema so the server checks the same fields as the page. Email access accepts the address the visitor supplies; it does not verify control of that mailbox. For a process that needs proven identity, use the platform's signed-in or link-based form access, covered in the Forms course. Don't describe an email-only submission as an authenticated customer account.
PUT /repository/create with datatype crm_form and data containing name, title, status open, authenticationType email, workflow equal to the definition name and schema. The schema properties fullName, email, orderNumber, bikeModel and problem are strings; email uses format email. All five are required. Do not put a workflow record ID in a field that expects its name. CRM Forms → Forms → Warranty claim opens the same saved configuration. Setup selects the workflow, Fields edits the questions, Access sets the admission rule.

PUT /repository/create
{
"datatype": "crm_form",
"isNew": true,
"data": {
"name": "wf-usecase-1011-warranty",
"title": "Warranty claim",
"status": "open",
"authenticationType": "email",
"allowGuest": true,
"workflow": "wf-usecase-1011-warranty",
"schema": {
"type": "object",
"properties": {
"fullName": {
"type": "string",
"title": "Full name"
},
"email": {
"type": "string",
"title": "Email",
"format": "email"
},
"orderNumber": {
"type": "string",
"title": "Order number"
},
"bikeModel": {
"type": "string",
"title": "Bike model"
},
"problem": {
"type": "string",
"title": "Problem"
}
},
"required": [
"fullName",
"email",
"orderNumber",
"bikeModel",
"problem"
]
}
}
}GET /client/forms/wf-usecase-1011-warranty
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /client/forms/wf-usecase-1011-warranty/submit
{
"fullName": "Dana Ruiz",
"email": "[email protected]",
"orderNumber": "HC-1042",
"bikeModel": "Harbor Trail 2",
"problem": "Frame crack after three weeks"
}6. Find the same configuration in Studio
These are the same records in Appmint Studio. Open CRM Forms and find Warranty claim. Setup shows the selected workflow and its decision stages. The Fields tab contains the five questions. Access shows the email rule the customer must satisfy. Submissions brings the saved answers and their status together. These management screens read the same records as our own client. Your team can use Studio while you build the customer experience with AppEngine.

7. Submit and approve through a real client
Here's the complete client journey. Dana enters her name, email, order number, bike model, and the problem. Send claim opens the email-first form and submits those answers. AppEngine returns a saved claim reference. Now switch to the reviewer desk. Priya signs in using her own staff account; the browser keeps that session separate from the public form. Her waiting tray contains the assigned claim. Opening it reads the saved answers and the current workflow position. She checks the order and the reported problem, adds a decision note, and approves. The client then reloads the source record and its workflow position. Approved is the saved result, and the history retains the prior stage, the reviewer and the decision note. This is the runnable example included with the course, backed by the actual AppEngine requests.
Run the supplied Node client with APPENGINE_URL, APPENGINE_ORG and FORM_NAME. The customer submits five answers. The reviewer signs in from Reviewer desk, opens a waiting item, reads the original answers and writes a note before Approve or Reject. The code reloads repository/get/form_submission/:id and workflow/position/form_submission/:id after the decision. Status in the UI is the actual server response, not a local optimistic success. The browser client is explicitly an example application, not a claimed Studio screen.

8. Read, validate and submit the public form
Now the requests behind that journey. An email-first form requires an address even when opening it. With no address, this form returns email required. Supply the email query parameter to read its schema, then submit answers under the exact field names. Leave out order number and the server returns a validation error naming that field. Leave out email and the access rule refuses the submission. A successful submit returns the saved record ID and the original answers. Next, staff read workflow position using form submission and that record ID. The returned task ID is different from the submission ID. Keep both: the record holds the answers; the task receives decisions. A successful save alone does not prove the workflow started, so the integration checks position too.
GET /client/forms/:name?email=... returns this email-first form only after the address is supplied. A missing address returns401 email-required. POST /client/forms/:name/submit contains the answer fields directly. A missing required orderNumber returns400. Store the returned submission sk, then use staff GET /workflow/position/form_submission/:id. A taskSk identifies the separate approval task. If position returns null, the answers may still be saved: follow Recovery.

GET /client/forms/wf-usecase-1011-warranty
No request body. The path/query values identify the record or form; use the caller identity described above.
GET /client/forms/wf-usecase-1011-warranty?email=dana.wf-usecase-1011%40training.local
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /client/forms/wf-usecase-1011-warranty/submit
{
"fullName": "Dana Ruiz",
"email": "[email protected]",
"bikeModel": "Harbor Trail 2",
"problem": "Frame crack after three weeks"
}POST /client/forms/wf-usecase-1011-warranty/submit
{
"fullName": "Dana Ruiz",
"orderNumber": "HC-1042",
"bikeModel": "Harbor Trail 2",
"problem": "Frame crack after three weeks"
}POST /client/forms/wf-usecase-1011-warranty/submit
{
"fullName": "Dana Ruiz",
"email": "[email protected]",
"orderNumber": "HC-1042",
"bikeModel": "Harbor Trail 2",
"problem": "Frame crack after three weeks"
}GET /workflow/position/form_submission/6acb15d0ba781cd23c2f6e52
No request body. The path/query values identify the record or form; use the caller identity described above.
9. Approve with the assigned reviewer and verify the result
Priya's bearer token reads the approval tray. Waiting on me lists her assigned decisions, and each task's owner identifies its source record. Read those answers before asking for a decision. Send approve to the task ID with a useful note. AppEngine checks who is deciding: Sam's unassigned attempt returned forbidden. Priya's request succeeds. Read the submission again and its status is approved. Read workflow position and the final stage, actor and history agree. Retrying the same decision returns conflict because it was already approved. Treat that as a reason to refresh the result, not to create a second submission or overwrite the status directly. The client verifies the stored outcome after the action.
GET /approval/tray with the REVIEWER token yields waitingOnMe. Each item.id is the task; item.owner.datatype/id is the source. POST /approval/task/:taskId/approve with {note} performs the decision. An unassigned ordinary staff token returns403. A completed task returns409 on repeat. On any ambiguous retry, reread position/source before showing completion or sending another operation.

GET /approval/tray
No request body. The path/query values identify the record or form; use the caller identity described above.
GET /repository/get/form_submission/6acb15d0ba781cd23c2f6e52
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb15d0ba781cd23c2f6e53/approve
{
"note": "Attempt by a reviewer not assigned to this task"
}POST /approval/task/6acb15d0ba781cd23c2f6e53/approve
{
"note": "Frame defect covered. Replacement frame approved."
}GET /repository/get/form_submission/6acb15d0ba781cd23c2f6e52
No request body. The path/query values identify the record or form; use the caller identity described above.
GET /workflow/position/form_submission/6acb15d0ba781cd23c2f6e52
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb15d0ba781cd23c2f6e53/approve
{
"note": "Repeated browser submission"
}10. Reject with a reason
Rejection is a first-class outcome. A second claim reaches the same waiting stage. Priya sends reject to that claim's task, with the reason that the damage is outside the warranty. The task enters Rejected and the source record becomes rejected while preserving the original answers. This is not deleting the request. Keeping the reason and the history lets your interface explain the result later. The approval service also initiates its notification path. Delivery needs the organization's configured channel: this training deployment disables external sending, so the verified outcome here is the saved decision, not a delivered customer email.
Submit a distinct claim, obtain its task ID and POST /approval/task/:taskId/reject with a clear note. Read the source: data.status rejected and state completed are preserved alongside the original values. Rejection retains a traceable record; deletion would not. The service initiates notifications but the recorded deployment has outgoing SMTP disabled. No delivered-email outcome is claimed.

GET /workflow/position/form_submission/6acb1605ba781cd23c2f6e72
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb1605ba781cd23c2f6e73/reject
{
"note": "Impact damage is outside this warranty."
}GET /repository/get/form_submission/6acb1605ba781cd23c2f6e72
No request body. The path/query values identify the record or form; use the caller identity described above.
11. Reassign an open decision
When Priya is away, she can hand an open decision to Sam. Send reassign with Sam's staff email and a note. AppEngine checks that the destination exists in the same organization. Sam's own tray now contains the request. His token approves it, and the history preserves both the handoff and the later decision. Reassignment changes responsibility; it doesn't manufacture a different customer submission. Use the approval route for an approval task, and refresh assignment after it succeeds. A completed decision should be read as a result, not reassigned as if it were still waiting.
POST /approval/task/:taskId/reassign with assignTo:[staffEmail] and note on an OPEN task. The destination must exist in the same organization and cannot be the requester for a protected approval. Read the destination user’s tray with THEIR token. Decide as that user and inspect history for the reassignment and final decision. Do not use a ConfigAdmin token to claim you tested ordinary reviewer authorization.

POST /approval/task/6acb1605ba781cd23c2f6e78/reassign
{
"assignTo": [
"[email protected]"
],
"note": "Sam will finish the review."
}GET /approval/tray
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb1605ba781cd23c2f6e78/approve
{
"note": "Warranty evidence checked."
}GET /workflow/position/form_submission/6acb1605ba781cd23c2f6e76
No request body. The path/query values identify the record or form; use the caller identity described above.
12. Escalate a waiting decision
Escalation adds help according to the stage's rule. This definition waits two days before adding Sam. Position shows the actual due date and current escalation tier. For an urgent claim, Priya can use the approval escalation action now. The response identifies the added reviewer, and a fresh position read confirms the changed assignment and tier. Sam can then decide the claim. We use this manual action to demonstrate the real escalation rule without pretending two days passed. A scheduled deadline and a manually triggered escalation share the stage configuration, but only the immediate action is exercised in this recording.
Read dueDate/escalationTier/assignTo first. POST /approval/task/:taskId/escalate applies the next configured tier now. The result reports added reviewers. Re-read position before allowing the new reviewer to decide. This recording demonstrates the immediate route; it does not pretend that a two-day timer elapsed. Scheduled processing and provider delivery have separate setup requirements.

GET /workflow/position/form_submission/6acb1606ba781cd23c2f6e7d
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb1606ba781cd23c2f6e7f/escalate
{}GET /workflow/position/form_submission/6acb1606ba781cd23c2f6e7d
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb1606ba781cd23c2f6e7f/reject
{
"note": "Escalated review complete."
}13. Require every assigned reviewer
Some decisions need two people. Use a separate definition with Priya and Sam in the assignment rule, and mode all. Both can see the assigned task. Priya's approval is recorded, but the response still names Sam in waiting on. The source remains in the waiting stage. Only after Sam approves does the task enter Approved and the source record become approved. The client must inspect the returned waiting list or reload position instead of treating every successful approve response as a final decision. One reviewer approving and the process finishing are different events.
Set start-stage assignRule.mode to all with both real staff emails. First approval201 includes waitingOn:[otherEmail] and leaves position.isFinal false. The second reviewer’s approval makes position.isFinal true. Check this result rather than assuming any201 approve response means the whole process completed. One rejection still ends the decision branch.

POST /workflow/definition
{
"name": "wf-usecase-1011-warranty-both",
"title": "Warranty \u2014 Two Reviewers",
"enabled": true,
"notificationTemplate": [],
"stages": [
{
"id": 0,
"type": "start",
"name": "Waiting for approval",
"modelState": "inprogress",
"modelStatus": "new",
"assignRule": {
"type": "users",
"users": [
"[email protected]",
"[email protected]"
],
"mode": "all"
},
"notificationTemplate": [],
"escalations": [
{
"escalateAfter": 2,
"units": "days",
"escalateRule": {
"type": "users",
"users": [
"[email protected]"
]
},
"notificationTemplate": []
}
]
},
{
"id": 1,
"type": "done",
"name": "Approved",
"modelState": "completed",
"modelStatus": "approved",
"notificationTemplate": []
},
{
"id": 2,
"type": "done",
"name": "Rejected",
"modelState": "completed",
"modelStatus": "rejected",
"notificationTemplate": []
}
]
}POST /approval/task/6acb1606ba781cd23c2f6e86/approve
{
"note": "First reviewer confirms the evidence."
}GET /workflow/position/form_submission/6acb1606ba781cd23c2f6e85
No request body. The path/query values identify the record or form; use the caller identity described above.
POST /approval/task/6acb1606ba781cd23c2f6e86/approve
{
"note": "Second reviewer approves."
}GET /workflow/position/form_submission/6acb1606ba781cd23c2f6e85
No request body. The path/query values identify the record or form; use the caller identity described above.
14. Recover a saved claim without resubmitting
A workflow configuration can be unavailable while the form still accepts answers. Here we temporarily disable only our course definition and submit one new claim. The submission succeeds, but position has no task. The customer's answers are safe; the process did not start. Restore the definition, then fire the workflow explicitly for that existing form submission ID. Include the workflow ID and a meaningful title. Position now returns its assigned task. Do not submit the form again to repair this situation: that creates another customer record. Your recovery path keeps the original ID, checks whether a task already exists, and starts only the missing process. Report a missing position as an integration issue rather than telling the customer their saved answers vanished.
The demonstrated failure disables only the course-owned definition, then submits once. The submission remains saved and position is null. Re-enable the definition; GET position first to avoid blindly firing an existing task. POST /workflow/fire with datatype form_submission, id equal to the EXISTING submission sk, workflowId equal to the saved definition sk and title. Read position to verify assignment. This creates the missing process for the original answers; it does not create another customer claim.

PATCH /workflow/definition/6acb15c8ba781cd23c2f6e42
{
"enabled": false
}POST /client/forms/wf-usecase-1011-warranty/submit
{
"fullName": "Dana Ruiz",
"email": "[email protected]",
"orderNumber": "HC-1047",
"bikeModel": "Harbor Trail 2",
"problem": "Frame crack after three weeks"
}GET /workflow/position/form_submission/6acb1606ba781cd23c2f6e8a
No request body. The path/query values identify the record or form; use the caller identity described above.
PATCH /workflow/definition/6acb15c8ba781cd23c2f6e42
{
"enabled": true
}POST /workflow/fire
{
"datatype": "form_submission",
"id": "6acb1606ba781cd23c2f6e8a",
"workflowId": "6acb15c8ba781cd23c2f6e42",
"title": "Recovered warranty claim HC-1047",
"data": {
"requesterEmail": "[email protected]"
}
}GET /workflow/position/form_submission/6acb1606ba781cd23c2f6e8a
No request body. The path/query values identify the record or form; use the caller identity described above.
15. Bring another system into the same process
The entry point can also be another application. Keep that boundary clear. A provider webhook first needs its provider-specific authenticity check; a URL receiving a request is not proof of who sent it. Your verified adapter maps the external event into this form's schema, then submits once and stores the returned claim ID against the external event ID. Retries should look up that mapping before making a second claim. After the record exists, the same workflow, reviewer assignment, decision and history apply. The downloadable adapter demonstrates this mapping and duplicate-event check. Provider registration, OAuth and signature details belong to the linked integration course because those contracts differ by provider.
The event adapter in the download verifies a CUSTOM HMAC protocol before calling submit, records event ID→claim ID, and returns the stored claim for a duplicate event. Its test verifies invalid signatures cause zero writes and a replay does not create another record. Replace verification with the actual provider SDK for your provider. The test’s in-memory map is not durable production deduplication. A deployed adapter needs a durable unique event key and recovery for a crash between submission and mapping persistence. See Integrations/OAuth/Webhooks and Custom API Gateway courses for provider setup.

16. Build on the saved process
You now have a connected AppEngine process: a defined form, validated answers, an assigned task, a decision that updates its record, and a history your application can show. The examples include both decisions, handoff, escalation, multiple reviewers and recovery. Use the chapter links when you need a particular operation, and keep the source record and its workflow position together when checking the result.

Runnable companions
Download all client and request source. Includes 53 observed requests and their complete payloads, five language/tool formats plus JSON, browser client, and a tested custom-event adapter.