Headless and agent onboarding
The supported headless flow sends a Morph API key, Morph organization id, and
repository identity to Morph. It does not send a user's GitHub token to
Morph. Keep any gh credential local.
Headless onboarding still contains a human checkpoint: a GitHub owner or repository administrator must approve the Morph GitHub App and select the exact private repository. An agent can stop at that checkpoint and resume from a new process by reading server state again.
The supported boundary is a private repository with trusted workflow authors.
For request and response schemas, use the curated
Actions onboarding API reference. It is generated
only from the public /v1/onboarding/* contract and intentionally excludes the
service-wide operator schema.
Repository ownership is proved by two checks together: the caller is an admin of the named Morph organization, and the Morph GitHub App can access the exact repository through the installation bound to that tenant. A repository name or user GitHub credential alone is not ownership proof.
Shell setup
Use strict shell behavior and keep secrets out of command arguments, URLs, logs, and support payloads:
set -euo pipefail
export ACTIONS_API="https://actions.svc.cloud.morph.so/v1"
export MORPH_ORG_ID="org_..."
export REPOSITORY="owner/private-repository"
export MORPH_API_KEY="..."
actions_curl() {
curl --fail-with-body --silent --show-error "$@" \
-H "Authorization: Bearer $MORPH_API_KEY"
}
Do not enable shell tracing while secrets are present.
1. Observe before mutating
actions_curl --get "$ACTIONS_API/onboarding/status" \
--data-urlencode "morph_org_id=$MORPH_ORG_ID" \
--data-urlencode "repository=$REPOSITORY" \
| jq '{api_version, status, reason_code, repository, github_installation,
project, migration, dispatch_readiness, verification, next_actions}'
Use typed fields for control flow:
ready: continue to migration or verification;- an action with
kind: open_url: pause for the named human actor; kind: retry_request: retry itsresume_endpointwhen its condition is met;status: failedwithreason_code: public_repository_unsupported,retryable: false, or a conflict: stop and follow the bounded guidance.
Do not parse message or description to decide the next step.
2. Create or recover repository setup
jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
'{morph_org_id: $morph_org_id, repository_full_name: $repository}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/github/setup" \
-H "Content-Type: application/json" \
--data-binary @- \
| jq '{api_version, status, reason_code, retryable, request_id,
repository, github_installation, project, next_actions}'
If next_actions contains open_url, show that URL only to the authorized
human. It points to a short-lived, single-use Morph handoff bound to the Morph
organization and requested repository—not a bare GitHub App URL. Treat the
complete URL as a bearer capability and never put it in logs or tickets.
After GitHub approval, discard the prior response and rerun status/setup. That works after an agent restart. Repeated setup observes existing App access and links before creating another handoff, so a successful installation does not need to be repeated.
3. Analyze and review migration
jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
'{morph_org_id: $morph_org_id, repository: $repository,
preferences: {shape: "small", static_morph_labels: true}}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/migration/analyze" \
-H "Content-Type: application/json" \
--data-binary @- \
| tee /tmp/morph-actions-analysis.json \
| jq '{proposed_runs_on, jobs_requiring_update, jobs_unsupported,
security_warnings, can_open_pull_request, manual_required,
existing_pull_request}'
Review workflow triggers, token permissions, secret use, third-party actions, containers/privileges, every changed label set, and every unsupported job. The analyzer performs supported runner-label rewriting, not a complete workflow safety assessment.
After review, submit the same canonical repository/source/preferences to
POST /onboarding/migration/pull-request. Morph reuses only an exactly matching
receipt and open pull request. A changed source, labels, base/head, or unrelated
reserved branch produces a conflict instead of overwriting work. Review and
merge the pull request in GitHub before dispatching its target ref.
4. Dispatch once and keep the receipt
Generate an idempotency key locally and persist the safe receipt id returned by the service:
export WORKFLOW_ID="morph-actions-smoke.yml"
export REF="main"
export DISPATCH_KEY="$(python3 -c 'import uuid; print(uuid.uuid4())')"
jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
--arg workflow_id "$WORKFLOW_ID" \
--arg ref "$REF" \
--arg idempotency_key "$DISPATCH_KEY" \
'{morph_org_id: $morph_org_id, repository: $repository,
workflow_id: $workflow_id, ref: $ref, inputs: {},
idempotency_key: $idempotency_key}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/dispatches" \
-H "Content-Type: application/json" \
--data-binary @- \
| tee /tmp/morph-actions-dispatch.json \
| jq '{status, receipt_id, idempotency_key, reused, lookup}'
If the client loses the response, look up the saved receipt with
GET /onboarding/dispatches/receipts/{receipt_id}. Reuse an idempotency key only
for the byte-equivalent canonical request. A changed or indeterminate request
conflicts rather than risking a duplicate GitHub dispatch.
5. Poll exact verification
Once the receipt/run lookup yields a GitHub run id, poll verification with a
bounded loop. Replace GITHUB_RUN_ID with the observed integer:
export GITHUB_RUN_ID="123456789"
for attempt in $(seq 1 30); do
response="$({
jq -n \
--arg morph_org_id "$MORPH_ORG_ID" \
--arg repository "$REPOSITORY" \
--argjson github_run_id "$GITHUB_RUN_ID" \
'{morph_org_id: $morph_org_id, repository: $repository,
github_run_id: $github_run_id}' \
| actions_curl -X POST "$ACTIONS_API/onboarding/verify" \
-H "Content-Type: application/json" --data-binary @-
} 2>/dev/null)"
state="$(jq -r '.status' <<<"$response")"
if [ "$state" = "verified" ]; then
jq -e '
.runner_v2_status == "succeeded" and
.runner_v2_assignment_state == "verified" and
(.github_job_id != null) and
(.runner_v2_actual_github_job_id == .github_job_id) and
.github_job_status == "completed" and
.github_job_conclusion == "success"
' <<<"$response" >/dev/null
break
fi
[ "$state" = "pending" ] || {
jq '{status, failure_reason, github_run_id, github_job_id}' <<<"$response"
exit 1
}
[ "$attempt" -lt 30 ] || exit 1
sleep 10
done
pending, mismatch, orphaned, and execution failure are not success. An
unrelated verified run in the same Morph organization is irrelevant.
Recovery and support
If GitHub installation succeeds but the callback cannot correlate, preserve the installation and rerun status/setup. Follow the typed outcome guidance in Troubleshooting. Send support only the request id, Morph org id, repository, and approximate time—never an API key, GitHub token, cookie, state value, or handoff URL.