Outcome
Create a draft role with the default pipeline (Applied, Screening, Interview, Offer, Hired, Rejected). Optional: team, location, remotePolicy (onsite, hybrid, remote), employmentType, a salary range (shown on the job board only when set; salaryCurrency is an ISO 4217 code, salaryPeriod defaults to year), description (plain text; blank lines make paragraphs, "- " lines make bullets), up to 20 application questions, hiringManagerUserId (a recruiter or hiring manager on the team) and requiresApproval (a recruiter must approve before it opens). A hiring manager can create roles only for themselves. The slug (public URL segment) is generated from the title when omitted. The role is not public until roles.open.
Before you start
- Verify identity and the pinned organization context, then discover the exact action and a granted Recruit workspace.
- The required application permission is workspace:write. Hiring-team roles and record visibility are enforced separately; a workspace grant alone does not grant every Recruit action.
- Organization and workspace identity come from verified context. Pass the workspace as resourceType/resourceId, separate from action input; request-body organizationId/workspaceId values cannot select an authorized scope.
- The first authorized action can initialize an identity-only workspace mirror and invoke the configured workspace-created callback. It does not create an Auth workspace or grant new access.
Recruiters can perform the full hiring workflow. Hiring managers see their managed roles and candidates; interviewers see assigned interviews and their candidates. Workspace owners and organization administrators act as recruiters. Use team.me to read the caller's effective role and allowed actions.
Discover the target
topolo whoami --json
topolo actions get app_topolo_recruit.roles.create --detail --json
topolo resources topolo-recruit --json
topolo workspaces list topolo-recruit --json
topolo actions get app_topolo_recruit.team.me --detail --json
topolo actions get app_topolo_recruit.roles.list --detail --json
topolo actions get app_topolo_recruit.team.list --detail --json
Request
POST /api/roles
Save input as payload.json. CLI and MCP action input includes path identifiers. The reviewed plan separates path/query fields from the HTTP body. Set RECRUIT_WORKSPACE_ID from granted resource discovery.
Inspect and test this action in the signed action console. Use its discovered schema and plan; a test response still requires the verification described below.
Input fields
| Field | Required | Type | Constraints |
|---|---|---|---|
title | Required | string | {"minLength":1,"maxLength":160} |
slug | Optional | string | {"pattern":"^a-z0-9?$"} |
team | Optional | Union | {} |
location | Optional | Union | {} |
remotePolicy | Optional | string | {"enum":["onsite","hybrid","remote"]} |
employmentType | Optional | string | {"enum":["full_time","part_time","contract","temporary","internship"]} |
salaryMin | Optional | Union | {} |
salaryMax | Optional | Union | {} |
salaryCurrency | Optional | Union | {} |
salaryPeriod | Optional | Union | {} |
description | Optional | string | {"maxLength":20000} |
questions | Optional | array | {"maxItems":20} |
hiringManagerUserId | Optional | Union | Topolo user id of a hiring-team member; inspect team.list before assigning a manager. {} |
requiresApproval | Optional | boolean | {} |
Exact input schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"slug": {
"type": "string",
"pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"
},
"team": {
"anyOf": [
{
"type": "string",
"maxLength": 120
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"type": "string",
"maxLength": 160
},
{
"type": "null"
}
]
},
"remotePolicy": {
"type": "string",
"enum": [
"onsite",
"hybrid",
"remote"
]
},
"employmentType": {
"type": "string",
"enum": [
"full_time",
"part_time",
"contract",
"temporary",
"internship"
]
},
"salaryMin": {
"anyOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 100000000
},
{
"type": "null"
}
]
},
"salaryMax": {
"anyOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 100000000
},
{
"type": "null"
}
]
},
"salaryCurrency": {
"anyOf": [
{
"type": "string",
"pattern": "^[A-Z]{3}$"
},
{
"type": "null"
}
]
},
"salaryPeriod": {
"anyOf": [
{
"type": "string",
"enum": [
"year",
"month",
"hour"
]
},
{
"type": "null"
}
]
},
"description": {
"type": "string",
"maxLength": 20000
},
"questions": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9_]{1,40}$"
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 300
},
"kind": {
"type": "string",
"enum": [
"short_text",
"long_text",
"yes_no"
]
},
"required": {
"type": "boolean"
}
},
"required": [
"id",
"label",
"kind",
"required"
],
"additionalProperties": false
}
},
"hiringManagerUserId": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 128
},
{
"type": "null"
}
]
},
"requiresApproval": {
"type": "boolean"
}
},
"required": [
"title"
],
"additionalProperties": false
}
Effects
- Creates one draft role, its version 1 and its default pipeline stages.
Interpret the response
Call roles.get with $.role.id and confirm status draft and the stored fields.
The response identifier path is $.role.id for role. Lists may be empty; an empty list is not a discovered target.
Exact success schema:
{
"type": "object",
"required": [
"role"
],
"properties": {
"role": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"slug": {
"type": "string"
},
"title": {
"type": "string"
},
"team": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"remotePolicy": {
"enum": [
"onsite",
"hybrid",
"remote"
]
},
"employmentType": {
"enum": [
"full_time",
"part_time",
"contract",
"temporary",
"internship"
]
},
"salaryMin": {
"type": [
"integer",
"null"
]
},
"salaryMax": {
"type": [
"integer",
"null"
]
},
"salaryCurrency": {
"type": [
"string",
"null"
]
},
"salaryPeriod": {
"type": [
"string",
"null"
],
"enum": [
"year",
"month",
"hour",
null
]
},
"description": {
"type": "string"
},
"questions": {
"type": "array",
"items": {
"type": "object"
}
},
"hiringManagerUserId": {
"type": [
"string",
"null"
]
},
"requiresApproval": {
"type": "boolean"
},
"status": {
"enum": [
"draft",
"pending_approval",
"open",
"paused",
"closed"
]
},
"approvedBy": {
"type": [
"string",
"null"
]
},
"approvedAt": {
"type": [
"string",
"null"
]
},
"openedAt": {
"type": [
"string",
"null"
]
},
"closedAt": {
"type": [
"string",
"null"
]
},
"version": {
"type": "integer"
},
"createdBy": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"stages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"roleId": {
"type": "string"
},
"name": {
"type": "string"
},
"kind": {
"type": "string"
},
"position": {
"type": "integer"
},
"scorecardCriteria": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
}
}
}
Worked examples
Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. A synthetic fixture identifier grants no access. Validate and inspect the planned request before executing. Commands with --confirm or MCP confirm:true assume the intended effect or data export has already been authorized.
Successful request
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.
Input · payload.json:
{
"title": "E"
}
CLI:
topolo actions validate app_topolo_recruit.roles.create --data @payload.json --json
topolo actions plan app_topolo_recruit.roles.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.roles.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_recruit.roles.create",
"input": {
"title": "E"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 201:
{
"role": {
"id": "role_d8281d6a43a04773bd59e6c23d064112",
"slug": "e",
"title": "E",
"team": null,
"location": null,
"remotePolicy": "onsite",
"employmentType": "full_time",
"salaryMin": null,
"salaryMax": null,
"salaryCurrency": null,
"salaryPeriod": null,
"description": "",
"questions": [],
"hiringManagerUserId": null,
"requiresApproval": false,
"status": "draft",
"approvedBy": null,
"approvedAt": null,
"openedAt": null,
"closedAt": null,
"version": 1,
"createdBy": "u_admin",
"createdAt": "2026-10-05T09:00:00.000Z",
"updatedAt": "2026-10-05T09:00:00.000Z"
},
"stages": [
{
"id": "stg_f66b90dbda904e35be8b334f27c423f7",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"name": "Applied",
"kind": "applied",
"position": 0,
"scorecardCriteria": []
},
{
"id": "stg_dac51922926f4c2eb6bb22c8ab6004ba",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"name": "Screening",
"kind": "screening",
"position": 1,
"scorecardCriteria": []
},
{
"id": "stg_2b063991ad4d40b081513ae595e4bd22",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"name": "Interview",
"kind": "interview",
"position": 2,
"scorecardCriteria": []
},
{
"id": "stg_2759887093c54df8b4e1a60c1ea4b2d9",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"name": "Offer",
"kind": "offer",
"position": 3,
"scorecardCriteria": []
},
{
"id": "stg_93e84a68ecf54897b2e461eab50fdb6b",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"name": "Hired",
"kind": "hired",
"position": 4,
"scorecardCriteria": []
},
{
"id": "stg_1bbbbbe8594f4bb1a0188e1f559a8ad5",
"roleId": "role_d8281d6a43a04773bd59e6c23d064112",
"name": "Rejected",
"kind": "rejected",
"position": 5,
"scorecardCriteria": []
}
]
}
HTTP 201. Call roles.get with $.role.id and confirm status draft and the stored fields.
Another supported request
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.
Input · payload.json:
{
"title": "Engineer"
}
CLI:
topolo actions validate app_topolo_recruit.roles.create --data @payload.json --json
topolo actions plan app_topolo_recruit.roles.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.roles.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_recruit.roles.create",
"input": {
"title": "Engineer"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 201:
{
"role": {
"id": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"slug": "engineer",
"title": "Engineer",
"team": null,
"location": null,
"remotePolicy": "onsite",
"employmentType": "full_time",
"salaryMin": null,
"salaryMax": null,
"salaryCurrency": null,
"salaryPeriod": null,
"description": "",
"questions": [],
"hiringManagerUserId": null,
"requiresApproval": false,
"status": "draft",
"approvedBy": null,
"approvedAt": null,
"openedAt": null,
"closedAt": null,
"version": 1,
"createdBy": "u_admin",
"createdAt": "2026-10-05T09:00:00.000Z",
"updatedAt": "2026-10-05T09:00:00.000Z"
},
"stages": [
{
"id": "stg_ab02310cdaeb413cb268d39c54f3dcc0",
"roleId": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"name": "Applied",
"kind": "applied",
"position": 0,
"scorecardCriteria": []
},
{
"id": "stg_ca2845055c534e10876f427933a9ee58",
"roleId": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"name": "Screening",
"kind": "screening",
"position": 1,
"scorecardCriteria": []
},
{
"id": "stg_5418ee6dd7e14535bae7cdea2d261668",
"roleId": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"name": "Interview",
"kind": "interview",
"position": 2,
"scorecardCriteria": []
},
{
"id": "stg_db42985ea58c40faa7eb1cfc305567ff",
"roleId": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"name": "Offer",
"kind": "offer",
"position": 3,
"scorecardCriteria": []
},
{
"id": "stg_e790a54539ba4370a1b7b44c0d211c75",
"roleId": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"name": "Hired",
"kind": "hired",
"position": 4,
"scorecardCriteria": []
},
{
"id": "stg_ff57b8a6945e4bf4aa93398c182775d8",
"roleId": "role_51b53e37da7248fcbe19d3c4e204bfc7",
"name": "Rejected",
"kind": "rejected",
"position": 5,
"scorecardCriteria": []
}
]
}
HTTP 201. Call roles.get with $.role.id and confirm status draft and the stored fields.
Refused request · hiring_team_role_required
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.
Input · payload.json:
{
"title": "Senior Backend Engineer",
"team": "Engineering",
"location": "London",
"remotePolicy": "hybrid",
"employmentType": "full_time",
"salaryMin": 80000,
"salaryMax": 100000,
"salaryCurrency": "GBP",
"description": "You will build our platform.\n\n- Go or TypeScript\n- Postgres"
}
CLI:
topolo actions validate app_topolo_recruit.roles.create --data @payload.json --json
topolo actions plan app_topolo_recruit.roles.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
Captured local response · HTTP 403:
{
"error": {
"code": "hiring_team_role_required",
"message": "Your hiring-team role in this workspace does not allow this action.",
"details": {
"teamRole": "interviewer",
"allowedRoles": [
"recruiter",
"hiring_manager"
]
}
}
}
HTTP 403. Preserve the exact response code and details. Do not claim success.
Refused request · invalid_input
Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.
Input · payload.json:
{
"title": "x",
"unknown": true
}
CLI:
topolo actions validate app_topolo_recruit.roles.create --data @payload.json --json
topolo actions plan app_topolo_recruit.roles.create --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
Captured local response · HTTP 422:
{
"error": {
"code": "invalid_input",
"message": "The input does not match the action schema.",
"details": [
{
"code": "unrecognized_keys",
"keys": [
"unknown"
],
"path": [],
"message": "Unrecognized key: \"unknown\""
}
]
}
}
HTTP 422. Preserve the exact response code and details. Do not claim success.
Verify the result
- Call roles.get with $.role.id and confirm status draft and the stored fields.
Errors and recovery
organization_context_required(HTTP 403): Select the organization authorized by the credential; do not send an organization id in the action input.permission_denied(HTTP 403): Grant the credential the required Recruit workspace permission.hiring_team_role_required(HTTP 403): Ask a recruiter to give the caller a hiring-team role that allows this action (team.members.set); error.details lists the allowed roles.workspace_required(HTTP 409): Create a Recruit workspace, or pass the workspace resource context.invalid_input(HTTP 422): Correct the fields named in error.details using inputSchema, then retry.invalid_salary(HTTP 422): Give salaryCurrency with a salary, and keep salaryMin at or below salaryMax.duplicate_question_id(HTTP 422): Give each question a unique id.hiring_manager_not_on_team(HTTP 422): Add the person as recruiter or hiring_manager with team.members.set first.role_slug_taken(HTTP 409): Choose another slug, or omit it to have one generated from the title.role_not_managed(HTTP 403): Only a recruiter or the role's hiring manager can change it.invalid_json(HTTP 400): Send a valid JSON object. The request is rejected before workspace resolution or record creation.
Authentication or gateway failures may occur before the Recruit handler. Preserve the exact discovered service error, request id and credential context. The local fixtures do not establish live credential authorization.
Recovery boundary
Close the role created in error; roles are never deleted so their history stays auditable.
Related actions
- Open a role: Open a draft, paused or closed role so it appears on the published job board and accepts applications. A role that requires approval and has not been approved moves to pending_approval instead (approvalRequired true) until a recruiter calls roles.approve.
- Close a role: Close a role (filled or cancelled). It leaves the job board immediately; candidates and history are kept.
Evidence
Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. Test capture provenance is retained with each fixture. API, database and integration outcomes require separate verification in the target environment.