Tasks are assignments that require action during the loan origination process. There are two types of tasks: borrower tasks (assigned to borrowers) and loan officer tasks (assigned to loan officers). Tasks guide users through exactly what’s needed for underwriting and loan processing.
Types of tasks
Borrower tasks
Borrower tasks are assignments that require borrowers to:
- Upload documents (pay stubs, bank statements, tax returns, etc.)
- Review and sign disclosures
- Provide additional information or clarification
Loan officer tasks
Loan officer tasks are assignments that require loan officers to:
- Upload documents on behalf of borrowers
- Review and verify documents
- Complete underwriting conditions
- Handle disclosure-related tasks
Tasks are automatically created as the loan progresses through processing and underwriting. Each task includes:
- Task type (document upload, disclosure review, etc.)
- Status (not started, in progress, completed)
- Description of what’s needed
- Due date (if applicable)
- Document links (for uploaded documents)
Retrieving tasks
Query both borrower tasks and loan officer tasks through the loan query:
query GetTasks($loanId: ID!) {
loan(id: $loanId) {
id
borrowers(first: 10) {
edges {
node {
id
personalInformation {
firstName
lastName
}
borrowerTasks {
id
title
status
type
priority
dueDate
completedAt
createdOn
borrowerFacingDescription
documentLinks {
id
title
url
}
details {
borrowerId
borrowerName
entityKind
}
}
}
}
}
loanOfficerDocumentTasks {
id
title
status
type
priority
dueDate
completedAt
createdOn
description
documentLinks {
id
title
url
}
details {
borrowerId
borrowerName
entityKind
}
}
}
}
Task types
| Task Type | Description | Action Required |
|---|
DocumentRequired | Borrower or loan officer must upload a document | Upload document via REST endpoint |
DocumentProcessing | A submitted document is being processed | Wait for processing to complete |
InputRequired | Additional input is needed from the borrower | Provide the requested input |
How to use these document specifications
Each document page provides detailed specifications including required data fields, suggested questions, and validation rules. You have three options for handling these documents:
- Build your own forms - Use the required data fields and suggested questions from each document page to build custom forms that collect the necessary information
- Validate existing forms - Use the required data fields and validation rules to verify that your existing forms include all necessary fields and meet validation requirements
- Use pre-made templates - Use Pylon’s pre-made templates out of the box (templates will be provided separately)
Uploading documents for borrower tasks
Borrower task documents are uploaded via a REST endpoint (not GraphQL):
Endpoint: POST /api/loan-applications/{loanId}/borrower-tasks/{taskId}/documents
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: multipart/form-data
Form field name: borrower-task-files
Example:
async function uploadBorrowerTaskDocument(loanId, taskId, files) {
const formData = new FormData();
files.forEach((file) => {
formData.append("borrower-task-files", file);
});
const response = await fetch(
`https://pylon.mortgage/api/loan-applications/${loanId}/borrower-tasks/${taskId}/documents`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
body: formData,
}
);
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
return response.json();
}
Uploading documents for loan officer tasks
Loan officer tasks may also require document uploads. The upload process is similar, but the endpoint and form field name may differ. Check the task’s documentUploadPath field for the specific upload endpoint.
Reassigning all loan officer tasks to the borrower
When a loan officer wants to hand off every outstanding loan-officer task on a loan to the borrower — for example, once initial task generation completes and the LO wants the borrower to drive uploads directly — use the reassignLoanOfficerTasks mutation. It flips every task on the loan whose assignedTo is LOAN_OFFICER to BORROWER in one call, in a single transaction.
mutation ReassignLoanOfficerTasks($loanId: ID!) {
loan {
reassignLoanOfficerTasks(input: { loanId: $loanId }) {
reassignedCount
}
}
}
loanId accepts either the loan’s ID or its friendly ID. The response’s reassignedCount is the number of tasks that were actually reassigned — 0 is a valid, non-error response when the loan has no outstanding loan-officer tasks.
What gets reassigned
Only tasks with assignedTo = LOAN_OFFICER are moved. Every other task on the loan is untouched by construction:
Task’s current assignedTo | Result |
|---|
LOAN_OFFICER | Flipped to BORROWER |
BORROWER | Unchanged |
PROCESSOR (internal) | Unchanged |
The mutation only updates each task’s assignedTo; taskName, status, dueDate, uploaded documents, and every other field are preserved.
Why taskName is intentionally preserved: the inbound task sync only
recomputes a task’s assignee when its taskName changes. Leaving the name
alone is what keeps a later re-sync from silently reverting the
reassignment.
This is a manual, per-loan action — call it explicitly when you want to
hand tasks over. It is not automatic, and it can be called on a frozen
loan.
Rejecting a submitted document
When a borrower uploads a document that doesn’t meet requirements — illegible,
the wrong document, or incomplete — reject it with the rejectDocument
mutation. Rejecting a document archives it and reopens the task it had
satisfied, so the borrower is prompted to upload a replacement.
mutation RejectDocument($input: RejectDocumentInput!) {
borrower {
rejectDocument(input: $input) {
taskId
}
}
}
Example variables:
{
"input": {
"loanId": "loan_...",
"documentId": "docmeta_...",
"archiveReasonNotes": "Pay stub is illegible — please re-upload a clearer scan."
}
}
| Field | Type | Required | Description |
|---|
loanId | ID | Yes | The loan’s ID or friendly ID. |
documentId | ID | Yes | The ID of the document to reject. Obtain it from a task’s documentLinks { id }. |
archiveReasonNotes | String | Yes | Free-text note explaining why the document is being rejected. |
borrowerId | ID | No | Optional. Not required to reject a document. |
Response
| Field | Type | Description |
|---|
taskId | ID | The task associated with the rejected document — the task that is reopened for the borrower to act on. |
Behavior
- The rejected document is moved to an archived state and its associated task is
reopened, so a fresh upload is required to satisfy it again.
archiveReasonNotes is recorded against the rejection and explains why the
document was not accepted.
Unlike task-status polling, this mutation is not a silent no-op when
nothing matches. If the documentId can’t be found on the loan, or the
document has no task associated with it, the mutation returns an error rather
than succeeding with an empty result.
rejectDocument requires the update:borrower scope, and can be called on a
frozen loan. It is not idempotent — rejecting a document that is already
archived will re-issue the rejection.
Task status
After uploading a document, the task status updates automatically. Poll the tasks query to verify the upload was successful:
NotStarted - Task has been created but no action taken
InProgress - Document upload in progress
Completed - Task is complete (document uploaded and verified)
AutomaticallyCancelled - Task was cancelled by the system
ManuallyCancelled - Task was cancelled manually
Polling for new tasks
Tasks are created automatically as the loan progresses. Poll tasks every 15-30 minutes to detect new tasks:
async function pollForNewTasks(loanId, previousTaskIds) {
const query = `
query GetTasks($loanId: ID!) {
loan(id: $loanId) {
borrowers(first: 10) {
edges {
node {
borrowerTasks {
id
title
status
type
}
}
}
}
loanOfficerDocumentTasks {
id
title
status
type
}
}
}
`;
const response = await fetch("https://pylon.mortgage/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ query, variables: { loanId } }),
});
const { data } = await response.json();
const borrowerTasks = data.loan.borrowers.edges.flatMap(
(edge) => edge.node.borrowerTasks
);
const allTasks = [...borrowerTasks, ...data.loan.loanOfficerDocumentTasks];
const currentTaskIds = new Set(allTasks.map((t) => t.id));
const newTasks = allTasks.filter((task) => !previousTaskIds.has(task.id));
return { newTasks, currentTaskIds };
}
Task details
Each task includes a details field that provides context about the entity the task is associated with:
borrowerId - The borrower this task is for (if applicable)
borrowerName - Display name of the borrower
entityKind - The type of entity (e.g., “BORROWER”, “LOAN”)
entityDisplayName - Human-readable name of the entity
Best practices
- Poll regularly - Check for new tasks every 15-30 minutes
- Notify users - Alert borrowers and loan officers when new tasks are assigned
- Track completion - Monitor task status to ensure all required documents are uploaded
- Handle errors - Implement retry logic for failed uploads
- Validate files - Check file types and sizes before uploading
- Monitor both types - Track both borrower tasks and loan officer tasks to ensure nothing is missed