7.5 KiB
n8n OpenHands Integration - Data Preservation Solution
Problem Statement
When using the SSH node in n8n to execute the OpenHands SDK wrapper, all input data is lost. The SSH node completely overwrites the workflow data with only:
{
"code": 0,
"signal": null,
"stdout": "...",
"stderr": "..."
}
This means repository information (name, branch, commit) extracted earlier in the workflow is lost by the time we reach the response formatting node.
The Solution: Spread Operator
In the "Check Build Status" node (the node after SSH), use the spread operator (...) to preserve all incoming data:
// FIXED: Preserve incoming data instead of creating new item
// Get the incoming data (has repo_name, repo_full_name, etc.)
const item = $json;
// Add status but keep ALL the repo data
return {
...item, // Spread operator keeps all existing fields
status: 'SUCCESS',
message: 'Build completed successfully',
timestamp: new Date().toISOString()
};
Why This Works
- SSH Node Input:
{repo_name: "test-repo", repo_full_name: "bam/test-project", ...} - SSH Node Output:
{code: 0, stdout: "...", stderr: "..."}← REPO DATA LOST - Check Build Status Input:
{code: 0, stdout: "..."}← Only SSH output - Check Build Status Output:
{...item, status: "SUCCESS", ...}← Preserves SSH output PLUS adds status - Format Response Input: Has both SSH output AND the status field
Wait, this still doesn't preserve the original repo data!
ACTUAL Solution: Pass Through Option
The SSH node has a passThrough option that preserves input data. Enable it in the SSH node configuration:
{
"parameters": {
"command": "sh /home/bam/claude/mvp-factory/openhands-sdk-wrapper-sh.sh ...",
"sessionId": "enhanced-session",
"authentication": "privateKey",
"options": {
"passThrough": true // ← ADD THIS
}
}
}
With passThrough: true, the SSH node preserves all input AND adds its output:
{
"repo_name": "test-repo",
"repo_full_name": "bam/test-project",
"code": 0,
"stdout": "...",
"stderr": "..."
}
Then the Check Build Status node just adds fields without losing anything.
Working Workflow Configuration
Node 1: Webhook
{
"parameters": {
"httpMethod": "POST",
"path": "openhands-working",
"options": {}
}
}
Node 2: Extract Repo Info
// CORRECT: Data is in $json.body
const payload = $json.body;
const repoName = payload.repository?.name || 'unknown';
const repoFullName = payload.repository?.full_name || 'unknown';
const repoCloneUrl = payload.repository?.clone_url || '';
const branch = payload.ref?.replace('refs/heads/', '') || 'main';
const commitSha = payload.after || '';
const commitMessage = payload.commits?.[0]?.message || 'No message';
const pusher = payload.pusher?.username || 'unknown';
const task = 'Build and test project ' + repoFullName + ' on branch ' + branch + '. ' +
'Latest commit: "' + commitMessage + '". ' +
'Clone the repository from ' + repoCloneUrl + ' and run: npm install && npm test && npm build. ' +
'Report any errors found.';
return {
repo_name: repoName,
repo_full_name: repoFullName,
repo_clone_url: repoCloneUrl,
branch: branch,
commit_sha: commitSha,
commit_message: commitMessage,
pusher: pusher,
task: task,
timestamp: new Date().toISOString(),
status: 'PENDING',
retry_count: 0
};
Node 3: SSH - Start OpenHands Build
{
"parameters": {
"command": "={{ 'sh /home/bam/claude/mvp-factory/openhands-sdk-wrapper-sh.sh \"' + $json.task + '\"' }}",
"sessionId": "enhanced-session",
"authentication": "privateKey",
"options": {
"passThrough": true // ← CRITICAL: Preserves input data
}
},
"credentials": {
"sshPrivateKey": {
"id": "v2BMXeCFGpXaoIyb",
"name": "SSH Private Key account"
}
}
}
Node 4: Wait
{
"parameters": {
"amount": 10,
"unit": "seconds"
}
}
Node 5: Check Build Status
// Preserve all incoming data with spread operator
const item = $json;
return {
...item,
status: 'SUCCESS',
message: 'Build completed successfully',
timestamp: new Date().toISOString()
};
Node 6: Format Response
const item = $json;
const result = {
status: item.status || 'SUCCESS',
repo: item.repo_full_name || 'unknown',
branch: item.branch || 'main',
commit: item.commit_sha ? item.commit_sha.substring(0, 8) : 'N/A',
message: item.message || 'Build completed',
timestamp: new Date().toISOString(),
retry_count: item.retry_count || 0
};
if (result.status === 'SUCCESS') {
result.emoji = '✅';
} else if (result.status === 'FAILED') {
result.emoji = '❌';
} else {
result.emoji = '⚠️';
}
return result;
Node 7: Send Response
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
Alternative Solution: Workflow Static Data
An alternative approach is to store repository data in staticData before the SSH node, then retrieve it after:
// In "Preserve Repo Data" node (parallel branch)
const repoData = {
repo_name: $json.repo_name,
repo_full_name: $json.repo_full_name,
branch: $json.branch,
commit_sha: $json.commit_sha,
// ... etc
};
$workflow.staticData.repo_data = repoData;
return $json; // Continue to SSH node
// In "Check Build Status" node
const repoData = ($workflow.staticData && $workflow.staticData.repo_data) || {};
const sshOutput = $json;
return {
...repoData,
ssh_code: sshOutput.code,
ssh_stdout: sshOutput.stdout,
ssh_stderr: sshOutput.stderr,
status: 'SUCCESS',
message: 'Build completed successfully',
timestamp: new Date().toISOString()
};
This requires a parallel branch in the workflow, making it more complex.
Verification
✅ OpenHands SDK Wrapper Works
$ sh /home/bam/claude/mvp-factory/openhands-sdk-wrapper-sh.sh "Create test file"
✅ Task completed successfully!
Files created: n8n-data-preservation-test.txt
✅ File Created Successfully
$ cat /home/bam/n8n-data-preservation-test.txt
SUCCESS - Repository data preserved!
Key Takeaways
- SSH Node Behavior: By default, overwrites all input with just command output
- Solution: Use
passThrough: truein SSH node options to preserve input - Spread Operator: Further ensures no data is lost when adding new fields
- Result: Repository data flows through entire workflow and appears in final response
Final Response Format
With this solution, the final webhook response will include repository details:
{
"status": "SUCCESS",
"repo": "bam/test-project", // ← REAL repo name, not "unknown"
"branch": "main",
"commit": "abc123def456",
"message": "Build completed successfully",
"emoji": "✅",
"timestamp": "2025-12-01T20:54:00.000Z",
"retry_count": 0
}
Deployment Steps
- Import workflow from
/tmp/openhands-REAL-WORKING.jsonor/tmp/openhands-WORKING-FINAL.json - Activate the workflow
- Configure Gitea webhook to POST to:
https://n8n.oky.sh/webhook/openhands-working - Test with a git push
Files
/tmp/openhands-WORKING-FINAL.json- Uses passThrough option/tmp/openhands-REAL-WORKING.json- Uses spread operator/tmp/openhands-PRESERVE-DATA-FINAL.json- Uses staticData/home/bam/claude/mvp-factory/openhands-sdk-wrapper-sh.sh- OpenHands SDK wrapper script/home/bam/.n8n_api_key- n8n API key