skills/jira-issue-to-pr/SKILL.md
This skill should be used when the user asks to "set up a Jira automation to create pull requests", "poll Jira for create-pr issues", "automatically create GitHub PRs from Jira tickets", "deploy a Jira issue-to-PR automation", "create a Jira to GitHub PR workflow", or mentions automating GitHub PR creation from a Jira label. Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: "create-pr") and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.
npx skillsauth add openhands/extensions jira-issue-to-prInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Deploys a cron automation that polls a Jira Cloud instance for open issues carrying a configurable label and, for each new issue, starts an OpenHands agent conversation that clones the GitHub repository specified in the ticket body, creates a branch, implements or placeholders the requested change, and opens a pull request. Once the conversation starts, it also posts a comment on the Jira ticket: "I'm on it: <conversation URL>".
POST /rest/api/3/search/jql on the Jira Cloud instance
to find open issues with the configured label.first_run_at baseline
timestamp in the KV store; any issue whose updated timestamp predates that baseline
is skipped (no backfill blast on first deploy). Using updated rather than created
means an old issue that has its label added after the automation is deployed will still
be picked up. Subsequent runs filter by both first_run_at and a KV-backed set of
already-processed issue keys. A max_new_per_run cap (default 5) limits conversations
started per cron firing as additional defense-in-depth.POST /api/conversations on the agent server
to start an independent agent conversation with a PR-creation prompt. The prompt
instructs the agent to extract the target GitHub repository (owner/repo) from the
ticket body.I'm on it: <conversation URL>.The polling run is lightweight (stdlib only, no SDK install); LLM costs are incurred only when new issues are actually found.
Before deploying, ensure the following are in place:
| Requirement | Details |
|---|---|
| Jira API token | Stored as an OpenHands secret (see Jira API token setup) |
| GitHub token | Must be stored as an OpenHands secret with repo + workflow scope so the spawned conversation can push branches and open PRs |
| Jira label | The label to watch for (default: create-pr) must exist in the Jira project |
| GitHub repo | The target repository must exist and the GitHub token must have write access |
Gather the following from the user before proceeding:
| Parameter | Example | Notes |
|---|---|---|
| jira_base_url | https://acme.atlassian.net | No trailing slash |
| jira_email | [email protected] | Atlassian account email for Basic auth |
| jira_token_secret | JIRA_CLOUD_KEY | Name of the OpenHands secret holding the API token |
| jira_label | create-pr | Label to watch for (optional, defaults to create-pr) |
| max_new_per_run | 5 | Max conversations dispatched per cron firing (optional, defaults to 5) |
| cron_schedule | */5 * * * * | Polling frequency in cron syntax |
Note: The GitHub repository is not configured here. Each Jira ticket body must include a reference to the target GitHub repo in
owner/repoformat (e.g.acme-org/backend). The spawned agent extracts it from the ticket text.
Create config.json next to scripts/main.py when packaging:
{
"jira_base_url": "https://acme.atlassian.net",
"jira_email": "[email protected]",
"jira_token_secret": "JIRA_CLOUD_KEY",
"jira_label": "create-pr",
"max_new_per_run": 5
}
Copy scripts/main.py from this skill and package it with the config.json:
WORK=$(mktemp -d)
cp <skill-dir>/scripts/main.py "$WORK/main.py"
# write config.json into $WORK/config.json (see Step 2)
tar -czf /tmp/jira-issue-to-pr.tar.gz -C "$WORK" .
python3 -m py_compile "$WORK/main.py" # validate syntax before uploading
TARBALL_PATH=$(curl -s -X POST \
"http://localhost:8000/api/automation/v1/uploads?name=jira-issue-to-pr" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
-H "Content-Type: application/gzip" \
--data-binary @/tmp/jira-issue-to-pr.tar.gz \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tarball_path'])")
curl -s -X POST "http://localhost:8000/api/automation/v1" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Jira issue-to-PR Poller\",
\"trigger\": {
\"type\": \"cron\",
\"schedule\": \"*/5 * * * *\",
\"timezone\": \"UTC\"
},
\"tarball_path\": \"$TARBALL_PATH\",
\"entrypoint\": \"python3 main.py\",
\"timeout\": 540
}" | python3 -m json.tool
Save the returned id - use it for updates and monitoring.
curl -s -X POST \
"http://localhost:8000/api/automation/v1/<AUTOMATION_ID>/dispatch" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" | python3 -m json.tool
# After ~30 seconds, check the run status:
curl -s "http://localhost:8000/api/automation/v1/<AUTOMATION_ID>/runs?limit=1" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
| python3 -c "import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))"
To change configuration or update the script:
config.json with new values.tarball_path:curl -s -X PATCH \
"http://localhost:8000/api/automation/v1/<AUTOMATION_ID>" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"tarball_path\": \"<NEW_TARBALL_PATH>\"}"
To reprocess issues that were already handled (e.g., after testing), clear the KV store:
curl -s -X DELETE \
"http://localhost:8000/api/automation/v1/<KV_BASE>/v1/kv/state" \
-H "Authorization: Bearer $AUTOMATION_KV_TOKEN"
Or delete and recreate the automation to start with a clean state.
The automation script lives at scripts/main.py. Key behaviors:
setup.sh or uv install needed.config.json co-located with the script.first_run_at (UTC timestamp) into the KV store and exits without dispatching; issues whose updated timestamp predates that baseline are skipped on all subsequent runs. Using updated (not created) means an old issue that has its label applied after deployment is correctly treated as new.max_new_per_run (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.{"processed_keys": [...], "first_run_at": "..."} between runs; falls back to a local file in dev environments where AUTOMATION_KV_TOKEN is absent.POST /rest/api/3/search/jql (the current non-deprecated endpoint).POST /api/conversations on the agent server with the current user's LLM/agent settings forwarded to the new conversation.The deduplication filter compares each issue's fields.updated timestamp against
first_run_at. updated is Jira's last-modified timestamp for the issue as a whole —
it advances whenever any field changes (comments, priority, description, status, etc.),
not only when the create-pr label is applied.
This means a pre-existing issue that already carried the label at deployment time can
slip through the filter if it is later updated for an unrelated reason (e.g. someone adds
a comment), because its updated timestamp will have advanced past first_run_at while
its key is not yet in processed_keys.
Workaround: The only fully reliable way to detect exactly when a label was applied
is the Jira changelog API (GET /rest/api/3/issue/{key}/changelog), which requires an
extra HTTP call per issue. To avoid that overhead, keep the automation's scope narrow:
use a label that is exclusively added as a PR-creation signal and is not already present
on issues at the time of deployment.
Once an issue is successfully dispatched its key is written to processed_keys in the
KV store and is permanently skipped on every future run — regardless of subsequent
label changes, comments, or any other updates to the issue. The only way to re-trigger a
previously processed issue is to manually clear the KV store or delete and recreate the
automation. This means the risk window described above is finite: as soon as the
automation processes a pre-existing issue (even accidentally), it will never dispatch
that issue again.
references/setup.md - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide.tools
Iterate on a GitHub pull request — drive it through CI, code review, and QA until it is merge-ready. Poll verification layers with `gh` CLI, diagnose and fix CI failures, address review feedback, retry flaky checks, push fixes, and repeat. The agent is the orchestration loop.
development
Guides technical explanations toward flowing, direct, conversational prose. This skill should be used for engineering chat, design discussion, architecture analysis, code-review explanations, and technical recommendations that should be concise without becoming fragmented or vague.
tools
This skill should be used when the user asks to "create an automation", "schedule a task", "set up a cron job", "webhook integration", "event-triggered automation", or mentions automations, scheduled tasks, cron scheduling, or webhook events in OpenHands Cloud.
testing
This skill should be used when the user asks to "write in plain English", "make this easier to read", "rewrite guidance", "improve report clarity", or produce accessible public-facing prose. It applies plain English content design principles: active voice, front-loaded content, sentence case, and no bold or italics for emphasis.