# Business Made · Readiness AI

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /business-made/training/ai/course-from-sop

**Draft a course from SOPs and policies**

`operationId: ReadinessAiController_courseFromSop`

Reads the chosen policies (content, sections and attached document) and documents (files in the org’s storage, by fileinfo id or path), and drafts a course with short modules and, by default, a multiple-choice quiz — using only what the sources say. The draft records the version of every source, so a later material change regenerates it. Uses the org’s AI integration (or the platform’s, metered to the org).

The course is a normal bm_course (`source: own`, `status: draft`, `type: self-paced`). Module text is in `content.modules[].sections[{title, body}]` and `content.modules[].body` (sections joined); the quiz is `assessment.items[{id, question, options[], answerIndex, explanation, moduleId}]` with `assessment.passingScore`. Translations sit beside the text: `content.modules[].translations[lang]` = `{title, description, body, sections}` and `assessment.items[].translations[lang]` = `{question, options, explanation}`. `generation` records `{generator: "readiness-ai", lineageId, previousVersionId?, supersededById?, sources:[{datatype: bm_policy|fileinfo|file, id, version, title, path?}], changedSources?, reason: initial|source_changed|manual, audience?, quiz, languages, model, generatedAt, generatedBy, publishedAt?, publishedBy?, warnings}`.

#### Signature

```http
POST /business-made/training/ai/course-from-sop (body) -> `{ course, sources:[{datatype, id, version, title, characters, warning?}], warnings:[] }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- About 120,000 characters of source text are used; anything cut is named in `warnings`.
- Translation failures do not fail the request; they appear in `warnings`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_COURSE_REQUEST | Choose at least one document or policy… | No sources, bad language code, or quizQuestions out of range. | `problems` lists each. |
| `404` | POLICY_NOT_FOUND | Policy not found | A policyId does not exist. | — |
| `422` | NO_SOURCE_TEXT | None of the chosen sources has any readable text. | Every source was empty, a scanned image, or an unsupported file type. | `warnings` says why for each source. PDF, DOCX, PPTX, TXT, MD and HTML are read. |
| `424` | AI_NOT_CONFIGURED | No AI integration is set up for this organization. | Neither the org nor the platform has an active AI integration. | Add an AI integration in Integrations. |
| `502` | AI_PROVIDER_ERROR | The AI provider did not answer. | The provider call failed after its own retries. | Try again; the message carries the provider’s reason. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/training/ai/courses/{id}/publish`
- `POST /business-made/training/ai/courses/{id}/translate`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "policyIds": [
    "POL-FOOD01"
  ],
  "documentIds": [
    "sops/closing-checklist.pdf"
  ],
  "title": "Closing the kitchen",
  "roles": [
    "Cook"
  ],
  "languages": [
    "es"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ course, sources:[{datatype, id, version, title, characters, warning?}], warnings:[] }` |
| `400` | Choose at least one document or policy… — No sources, bad language code, or quizQuestions out of range. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Policy not found — A policyId does not exist. |
| `422` | None of the chosen sources has any readable text. — Every source was empty, a scanned image, or an unsupported file type. |
| `424` | No AI integration is set up for this organization. — Neither the org nor the platform has an active AI integration. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `502` | The AI provider did not answer. — The provider call failed after its own retries. |

## GET /business-made/training/ai/courses

**List generated courses by lineage**

`operationId: ReadinessAiController_courses`

One row per course lineage (all versions of one generated course): the live version, the pending draft if any, and every version with its sources, what changed and why it was made.

#### Signature

```http
GET /business-made/training/ai/courses (status?: string, lineage?: string) -> `{ data:[{ lineageId, title, live, draft, versions:[{id, version, status, reason, generatedAt, publishedAt, languages, modules, questions, sources, changedSources, warnings}] }], total }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `status` | query | string | — | Only lineages with a version in this status (draft, active, archived). |
| `lineage` | query | string | — | One lineage (the sk of its first version). |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ data:[{ lineageId, title, live, draft, versions:[{id, version, status, reason, generatedAt, publishedAt, languages, modules, questions, sources, changedSources, warnings}] }], total }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/training/ai/courses/{id}/sources

**Sources of a course and whether they changed**

`operationId: ReadinessAiController_sources`

Each source the version was built from, the version it was read at, the version it is at now, and `changed`.

#### Signature

```http
GET /business-made/training/ai/courses/{id}/sources (id: string) -> `{ courseId, version, sources:[{datatype, id, title, version, currentVersion, changed}] }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | COURSE_NOT_FOUND | Course not found | No course has that id. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | bm_course sk (any version of a generated course). |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ courseId, version, sources:[{datatype, id, title, version, currentVersion, changed}] }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Course not found — No course has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/training/ai/courses/{id}/regenerate

**Regenerate a course from its sources**

`operationId: ReadinessAiController_regenerate`

Rebuilds the course from the current versions of its sources, keeping audience, quiz and languages. If the lineage already has an unpublished draft, that draft is rebuilt in place; otherwise a new draft version is created (version + 1, `previousVersionId` = the live one). This also happens on its own when a source changes materially: a policy saved with a new changeLog entry marked `material`, a replaced policy document, or an updated fileinfo record.

#### Signature

```http
POST /business-made/training/ai/courses/{id}/regenerate (id: string) -> `{ course, replacedDraft }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | COURSE_NOT_FOUND | Course not found | No course has that id. | — |
| `400` | NOT_GENERATED | Only courses built from SOPs can be regenerated. | The course was not made by course-from-sop. | Edit or publish it from Learning instead. |
| `422` | NO_SOURCE_TEXT | None of the chosen sources has any readable text. | Every source was empty, a scanned image, or an unsupported file type. | `warnings` says why for each source. PDF, DOCX, PPTX, TXT, MD and HTML are read. |
| `424` | AI_NOT_CONFIGURED | No AI integration is set up for this organization. | Neither the org nor the platform has an active AI integration. | Add an AI integration in Integrations. |
| `502` | AI_PROVIDER_ERROR | The AI provider did not answer. | The provider call failed after its own retries. | Try again; the message carries the provider’s reason. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/training/ai/check-sources`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | bm_course sk (any version of a generated course). |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ course, replacedDraft }` |
| `400` | Only courses built from SOPs can be regenerated. — The course was not made by course-from-sop. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Course not found — No course has that id. |
| `422` | None of the chosen sources has any readable text. — Every source was empty, a scanned image, or an unsupported file type. |
| `424` | No AI integration is set up for this organization. — Neither the org nor the platform has an active AI integration. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `502` | The AI provider did not answer. — The provider call failed after its own retries. |

## POST /business-made/training/ai/courses/{id}/translate

**Translate a course**

`operationId: ReadinessAiController_translate`

Adds (or replaces) translations of the modules and quiz for each language. The languages are remembered, so regenerated versions are translated too.

#### Signature

```http
POST /business-made/training/ai/courses/{id}/translate (id: string, body) -> `{ course, languages, warnings }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | COURSE_NOT_FOUND | Course not found | No course has that id. | — |
| `400` | INVALID_LANGUAGES | Not a language code | Empty list or a value that is not a language code. | — |
| `424` | AI_NOT_CONFIGURED | No AI integration is set up for this organization. | Neither the org nor the platform has an active AI integration. | Add an AI integration in Integrations. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | bm_course sk (any version of a generated course). |

### Request body

```json
{
  "languages": [
    "es",
    "vi"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ course, languages, warnings }` |
| `400` | Not a language code — Empty list or a value that is not a language code. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Course not found — No course has that id. |
| `424` | No AI integration is set up for this organization. — Neither the org nor the platform has an active AI integration. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/training/ai/courses/{id}/publish

**Publish a generated course version**

`operationId: ReadinessAiController_publish`

Makes the draft live. When it replaces a live version: the old version is archived (its completions stand), every requirement rule that named the old version now names this one, and only the people those rules cover (roles, locations and the rest of each rule’s scope) are asked to redo it — a new enrollment cycle for those who had completed it, open enrollments moved to the new version. For each source policy whose version changed, people covered by that policy’s rules who acknowledged an older version get a pending acknowledgement of the current one. People who were never assigned are left to the readiness sync. The affected rules are then re-synced.

#### Signature

```http
POST /business-made/training/ai/courses/{id}/publish (id: string, body) -> `{ course, superseded:[ids], rulesMoved:[{id,title}], rulesUsingCourse:[{id,title,status}], requiz:{enrollmentsCreated, enrollmentsMoved, byRule:[{ruleId,title,covered,enrollmentsCreated,enrollmentsMoved,dueDate}]}, reacknowledge:{acknowledgementsCreated, byRule:[{ruleId,title,policyId,policyVersion,covered,acknowledgementsCreated,dueDate}]} }`. `rulesUsingCourse` empty = nobody is required to take it yet.
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | COURSE_NOT_FOUND | Course not found | No course has that id. | — |
| `400` | NOT_GENERATED | Only courses built from SOPs can be regenerated. | The course was not made by course-from-sop. | Edit or publish it from Learning instead. |
| `409` | NOT_DRAFT | This version is not a draft. | It is already live or archived. | — |
| `422` | COURSE_EMPTY | The course has no modules to publish. | The draft has no module content. | — |
| `503` | READINESS_UNAVAILABLE | The readiness engine is not loaded. | Moving rules and enrollments needs the readiness engine, and it is not running in this deployment. | Retry later. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/readiness/rules`
- `POST /business-made/readiness/sync`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | bm_course sk (any version of a generated course). |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ course, superseded:[ids], rulesMoved:[{id,title}], rulesUsingCourse:[{id,title,status}], requiz:{enrollmentsCreated, enrollmentsMoved, byRule:[{ruleId,title,covered,enrollmentsCreated,enrollmentsMoved,dueDate}]}, reacknowledge:{acknowledgementsCreated, byRule:[{ruleId,title,policyId,policyVersion,covered,acknowledgementsCreated,dueDate}]} }`. `rulesUsingCourse` empty = nobody is required to take it yet. |
| `400` | Only courses built from SOPs can be regenerated. — The course was not made by course-from-sop. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Course not found — No course has that id. |
| `409` | This version is not a draft. — It is already live or archived. |
| `422` | The course has no modules to publish. — The draft has no module content. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `503` | The readiness engine is not loaded. — Moving rules and enrollments needs the readiness engine, and it is not running in this deployment. |

## POST /business-made/training/ai/check-sources

**Regenerate courses whose sources changed**

`operationId: ReadinessAiController_checkSources`

Files can change without any record being saved (a new upload to the same path), so this compares every generated course’s recorded source versions with the current ones and makes a new draft for each stale course. Pass `courseId` to check one.

#### Signature

```http
POST /business-made/training/ai/check-sources (body) -> `{ checked, regenerated:[{lineageId, from, draft, staleSources}] }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ checked, regenerated:[{lineageId, from, draft, staleSources}] }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/training/ai/new-hire

**A new hire’s onboarding and training state**

`operationId: ReadinessAiController_newHireState`

Everything a new hire might ask about: their onboarding plan (stages, tasks, what others are doing for them) and required training, with counts of overdue and due-soon items and the next things due — computed on the server. Without `employee`/`email` it is the caller’s own. Naming someone else needs an admin role or an AI employee identity; this is how an AI employee answers a new hire who messages it.

#### Signature

```http
GET /business-made/training/ai/new-hire (employee?: string, email?: string) -> `{ employee:{id, name, email, location, position, department, startDate}, onboarding: <GET staff-portal/onboarding shape> \| null, training: {items:[...]} (GET staff-portal/training shape), summary:{ onboarding:{done,total,overdue}, training:{total, done, overdue, dueSoon}, next:[{kind, title, dueDate, status}], text } }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | NOT_ALLOWED | You can only see your own onboarding. | A person who is not an admin or AI employee named someone else. | — |
| `404` | EMPLOYEE_NOT_FOUND | Employee not found | No employee matches, or the caller has no employee record. | — |

Plus the standard platform errors: `401`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `employee` | query | string | — | Employee badge code or bm_employee sk. |
| `email` | query | string | — | The person’s email (as seen in the conversation). |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ employee:{id, name, email, location, position, department, startDate}, onboarding: <GET staff-portal/onboarding shape> \| null, training: {items:[...]} (GET staff-portal/training shape), summary:{ onboarding:{done,total,overdue}, training:{total, done, overdue, dueSoon}, next:[{kind, title, dueDate, status}], text } }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You can only see your own onboarding. — A person who is not an admin or AI employee named someone else. |
| `404` | Employee not found — No employee matches, or the caller has no employee record. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

