Code-to-Photon: Workflow Automation Made Permanent
Version: 2.3.0+ Status: Available - Production Ready
Overview
The Code-to-Photon workflow transforms your one-time code executions into reusable, discoverable Photon tools. Write code once, save it, and reuse it infinitely.
Problem This Solves
- Repetitive Code: Write the same automation multiple times
- Tool Proliferation: Creates custom tools without npm publishing
- Workflow Capture: Convert successful executions into permanent tools
- Team Knowledge: Share automated workflows as Photons with your team
Solution: Executable Code Becomes a Tool
Write Code → Execute → Success! → Save as Photon → Use Forever
↓
Available in future workflowsQuick Start: Code → Photon in 3 Steps
Step 1: Write & Execute Code
Use code:run to execute your automation:
// Find trending GitHub repos
const repos = await github.search_repos({
query: 'stars:>10000 language:javascript',
sort: 'stars',
order: 'desc'
});
return repos.map(r => ({
name: r.name,
stars: r.stargazers_count,
url: r.html_url
}));Response:
{
"result": [
{ "name": "react", "stars": 203000, "url": "..." },
{ "name": "vue", "stars": 207000, "url": "..." }
],
"logs": ["Searched GitHub API..."],
"runId": "20260306_abc123xyz" // ← Save this!
}Step 2: Save as Photon
Once your code works, convert it to a reusable Photon:
const saved = await code.save_as_photon({
runId: "20260306_abc123xyz",
photonName: "github-trending",
description: "Find trending GitHub repos by stars"
});Result:
{
"success": true,
"photonPath": "/Users/you/.ncp/photons/github-trending.photon.ts",
"message": "Photon saved to /Users/you/.ncp/photons/github-trending.photon.ts. Available as 'github-trending' after restart."
}Step 3: Use as a Tool
Restart NCP and your Photon is immediately available:
// Now 'github-trending' is discoverable
const trending = await github_trending.run({});
// Or through code:
const results = await find({ description: "trending repos" });
// → includes github-trending toolComplete Workflow Examples
Example 1: Email Digest Generator
Initial execution:
const emails = await gmail.list_messages({
query: 'is:unread label:Important',
limit: 10
});
const summary = emails.map(e => ({
from: e.from,
subject: e.subject,
preview: e.snippet
}));
return {
count: emails.length,
emails: summary,
timestamp: new Date().toISOString()
};Save as Photon:
await code.save_as_photon({
runId: "run123",
photonName: "daily-email-digest",
description: "Generate digest of important unread emails"
});Now reuse it:
// Schedule it
await schedule.create({
name: "daily-digest",
schedule: "0 8 * * *", // 8 AM daily
tool: "daily-email-digest:run",
parameters: {}
});
// Or call manually
const digest = await daily_email_digest.run({});Example 2: Multi-Tool Orchestration
Initial code:
// Get GitHub stats
const ghStats = await github.get_user({ username: 'anthropics' });
// Get recent tweets
const tweets = await twitter.search({
query: 'Anthropic',
count: 5
});
// Save to spreadsheet
const result = await sheets.append_rows({
spreadsheet_id: "...",
range: "Sheet1",
values: [[
ghStats.public_repos,
tweets.length,
new Date().toISOString()
]]
});
return { status: 'saved', ghRepos: ghStats.public_repos, tweets: tweets.length };Save as Photon:
await code.save_as_photon({
runId: "run456",
photonName: "anthropic-metrics",
description: "Track Anthropic GitHub and Twitter metrics in spreadsheet"
});Schedule it:
await schedule.create({
name: "weekly-metrics",
schedule: "0 9 * * 1", // Monday 9 AM
tool: "anthropic-metrics:run",
parameters: {}
});Code Execution State & Resumability
Understanding Runs
Every code execution creates a run with:
- runId: Unique identifier (e.g.,
20260306_abc123xyz) - Code: Your TypeScript source code
- Start time: When execution began
- Result: Output value (if successful)
- Error: Error message (if failed)
- Logs: Console output during execution
Listing Runs
See recent executions:
const runs = await code.list_runs({ limit: 10 });
// Returns:
// [
// { runId: "20260306_abc123xyz", startedAt: "...", status: "completed" },
// { runId: "20260306_def456uvw", startedAt: "...", status: "completed" },
// ...
// ]Inspecting Run Details
Get full execution information:
const run = await code.get_run({
runId: "20260306_abc123xyz"
});
// Returns:
// {
// "runId": "20260306_abc123xyz",
// "tool": "code",
// "params": { "code": "const repos = await github..." },
// "startedAt": "2026-03-06T10:30:00Z",
// "completedAt": "2026-03-06T10:30:05Z",
// "status": "completed"
// }Best Practices
✅ Do
Make code idempotent:
// Good - same result on retry
const config = await settings.get_config({ key: 'user' });
await settings.update_config({ key: 'user', value: newValue });Log intermediate steps:
console.log('Starting GitHub search...');
const repos = await github.search_repos({ ... });
console.log(`Found ${repos.length} repos`);Return structured data:
return {
success: true,
count: items.length,
items: items,
timestamp: new Date().toISOString()
};Test before saving:
// Run multiple times to ensure consistency
const result1 = await code.run({ code: "return 42;" });
const result2 = await code.run({ code: "return 42;" });
// Both should be identical
// Then save
await code.save_as_photon({ runId, photonName, description });❌ Don't
Store secrets in code:
// Bad - credentials in code
const auth = "sk-1234567890abcdef";Use hardcoded paths:
// Bad - won't work on other machines
const file = "/Users/john/documents/file.txt";
// Good - use API parameters
const file = params.filePath;Rely on timing:
// Bad - race condition
await api.create({ ... });
setTimeout(() => { /* ... */ }, 100); // Too fragile!
// Good - poll or use callbacks
const result = await api.create({ ... });
while (!result.ready) {
await new Promise(r => setTimeout(r, 100));
result = await api.get({ id: result.id });
}State Persistence with InstanceStore
Photons can maintain state across executions:
export default class MyPhoton extends Photon {
private state = { count: 0, lastRun: null };
getState() {
return this.state;
}
setState(newState) {
this.state = { ...this.state, ...newState };
}
async run(params) {
// State is automatically restored from previous execution
this.state.count++;
this.state.lastRun = new Date();
return {
runCount: this.state.count,
lastRun: this.state.lastRun
};
}
}How it works:
- First execution: Creates initial state
- NCP calls
getState()and saves to~/.photon/state/{photonName}/ - Next execution: NCP calls
setState()with saved state - Your Photon resumes with previous state intact
Integration with Daemon
When photon daemon is running (photon daemon or photon beam):
Event Routing:
// Your Photon emits event
await this.emit({
type: 'metrics_updated',
data: { count: 42, timestamp: new Date() }
});
// Another Photon subscribes
async onNotification(eventType, payload) {
if (eventType === 'metrics_updated') {
console.log(`Metrics updated: ${payload.count}`);
}
}Cross-Process Communication:
- Events flow through daemon's DaemonBroker
- Photons in different processes can coordinate
- When daemon not running: fallback to NoOpBroker (local-only events)
Troubleshooting
"Save-as-Photon failed: Run not found"
Cause: The runId doesn't exist or expired Solution: Use code:list-runs to find valid runId, then try again
const runs = await code.list_runs({ limit: 20 });
const validRunId = runs[0].runId; // Use most recent
await code.save_as_photon({ runId: validRunId, ... });"Photon not appearing after save"
Cause: NCP needs to reload Photons Solution: Restart NCP or toggle Photon runtime in settings
# Option 1: Restart NCP
ncp list # Forces fresh load
# Option 2: Or in Claude Desktop settings
# Toggle: NCP → Settings → Enable Photon Runtime (toggle off/on)"State not persisting across runs"
Cause: Photon missing getState() / setState() methods Solution: Implement state methods:
export default class MyPhoton extends Photon {
private state = { /* initial state */ };
getState() { return this.state; }
setState(newState) { this.state = { ...this.state, ...newState }; }
async run(params) { /* ... */ }
}"DaemonBroker events not routing"
Cause: Photon daemon not running Solution: Start the daemon:
# In another terminal
photon daemon
# Or use Beam
photon beamAPI Reference
code:run
Execute TypeScript code with MCP access.
const result = await code.run({
code: "string", // Required: TypeScript code
timeout: 30000 // Optional: ms (default 30000, max 300000)
});
// Returns:
{
result: any, // Execution result
logs: string[], // Console output
runId: string, // Unique run identifier
error?: string // Error message if failed
}code:list-runs
List recent code execution runs.
const runs = await code.list_runs({
limit: 10 // Optional: max results (default 10)
});
// Returns:
{
runs: [
{
runId: string,
startedAt: string,
completedAt: string,
status: string
},
...
]
}code:get-run
Get details of a specific run.
const run = await code.get_run({
runId: string // Required: run identifier
});
// Returns:
{
runId: string,
tool: string,
params: object, // Original parameters
startedAt: string,
completedAt: string,
status: string
}code:save-as-photon
Convert code execution into reusable Photon.
const result = await code.save_as_photon({
runId: string, // Required: run ID to convert
photonName: string, // Required: tool name (kebab-case)
description: string // Required: what this tool does
});
// Returns:
{
success: boolean,
photonPath: string, // Path to created .photon.ts file
photonName: string,
message: string
}See Also
- Photon Runtime - Custom TypeScript MCPs
- Code Mode - Safe code execution
- Scheduling - Run Photons on schedule
- Daemon Architecture - Cross-process events