2424 "model": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
2525 "max_tokens": 10000
2626 },
27+ "g1_training": {
28+ "cron": "0 2 * * *",
29+ "enabled": true,
30+ "prompt": "Run G1 humanoid RL training",
31+ "workflow": "thor.yml"
32+ },
33+ "sim_validation": {
34+ "cron": "0 6 * * 1",
35+ "enabled": true,
36+ "prompt": "Validate robot models in Isaac Sim",
37+ "workflow": "isaac-sim.yml"
38+ },
2739 "deploy_friday": {
2840 "run_at": "2024-01-19T15:00:00Z",
2941 "enabled": true,
@@ -126,9 +138,7 @@ def _get_github_variable(repository: str, name: str, token: str) -> dict[str, An
126138 return {"success" : False , "message" : str (e )}
127139
128140
129- def _set_github_variable (
130- repository : str , name : str , value : str , token : str
131- ) -> dict [str , Any ]:
141+ def _set_github_variable (repository : str , name : str , value : str , token : str ) -> dict [str , Any ]:
132142 """Create or update a GitHub repository variable."""
133143 headers = {
134144 "Accept" : "application/vnd.github+json" ,
@@ -138,19 +148,15 @@ def _set_github_variable(
138148
139149 # Try to update first
140150 url = f"{ GITHUB_API_URL } /repos/{ repository } /actions/variables/{ name } "
141- response = requests .patch (
142- url , headers = headers , json = {"name" : name , "value" : value }, timeout = 30
143- )
151+ response = requests .patch (url , headers = headers , json = {"name" : name , "value" : value }, timeout = 30 )
144152
145153 if response .status_code == 204 :
146154 return {"success" : True , "message" : f"Variable { name } updated" }
147155
148156 # If not found, create it
149157 if response .status_code == 404 :
150158 url = f"{ GITHUB_API_URL } /repos/{ repository } /actions/variables"
151- response = requests .post (
152- url , headers = headers , json = {"name" : name , "value" : value }, timeout = 30
153- )
159+ response = requests .post (url , headers = headers , json = {"name" : name , "value" : value }, timeout = 30 )
154160 if response .status_code == 201 :
155161 return {"success" : True , "message" : f"Variable { name } created" }
156162
@@ -280,22 +286,14 @@ def _get_schedules(repository: str, token: str) -> dict[str, Any]:
280286 return {"jobs" : {}, "timezone" : "UTC" }
281287
282288 try :
283- return (
284- json .loads (result ["value" ])
285- if result ["value" ]
286- else {"jobs" : {}, "timezone" : "UTC" }
287- )
289+ return json .loads (result ["value" ]) if result ["value" ] else {"jobs" : {}, "timezone" : "UTC" }
288290 except json .JSONDecodeError :
289291 return {"jobs" : {}, "timezone" : "UTC" }
290292
291293
292- def _save_schedules (
293- repository : str , schedules : dict [str , Any ], token : str
294- ) -> dict [str , Any ]:
294+ def _save_schedules (repository : str , schedules : dict [str , Any ], token : str ) -> dict [str , Any ]:
295295 """Save schedules to GitHub variable."""
296- return _set_github_variable (
297- repository , SCHEDULES_VARIABLE , json .dumps (schedules , indent = 2 ), token
298- )
296+ return _set_github_variable (repository , SCHEDULES_VARIABLE , json .dumps (schedules , indent = 2 ), token )
299297
300298
301299@tool
@@ -312,12 +310,14 @@ def scheduler(
312310 max_tokens : int | None = None ,
313311 context : str | None = None ,
314312 repository : str | None = None ,
313+ workflow : str | None = None ,
315314) -> dict [str , Any ]:
316315 """Manage scheduled jobs for the autonomous agent.
317316
318317 This tool manages a schedule of jobs stored in GitHub Action variables.
319318 Jobs can be recurring (cron) or one-time (run_at).
320- The control loop workflow checks this schedule hourly and dispatches jobs.
319+ The control loop workflow checks this schedule hourly and dispatches jobs
320+ to the appropriate workflow file based on the "workflow" field.
321321
322322 Actions:
323323 - "list": List all scheduled jobs
@@ -341,6 +341,10 @@ def scheduler(
341341 max_tokens: Max tokens for model response
342342 context: Additional context to include in the prompt
343343 repository: GitHub repository (defaults to GITHUB_REPOSITORY env var)
344+ workflow: Target workflow file to dispatch to (default: "agent.yml").
345+ Use "thor.yml" for Thor GPU/hardware tasks, "isaac-sim.yml" for
346+ Isaac Sim simulation tasks, or any other workflow filename that
347+ supports workflow_dispatch with prompt/model/tools inputs.
344348
345349 Returns:
346350 Dict with status and operation results
@@ -350,7 +354,7 @@ def scheduler(
350354 # List all jobs
351355 scheduler(action="list")
352356
353- # Add a recurring daily code review job at 9 AM UTC
357+ # Add a recurring daily code review job at 9 AM UTC (runs on agent.yml / ubuntu)
354358 scheduler(
355359 action="add",
356360 job_id="daily_review",
@@ -360,6 +364,25 @@ def scheduler(
360364 tools="strands_tools:shell;strands_coder:use_github"
361365 )
362366
367+ # Schedule a GPU task on Thor (self-hosted runner)
368+ scheduler(
369+ action="add",
370+ job_id="g1_training",
371+ cron="0 2 * * *",
372+ prompt="Run G1 humanoid RL training with Newton backend",
373+ workflow="thor.yml",
374+ tools="strands_tools:shell,file_read,file_write"
375+ )
376+
377+ # Schedule an Isaac Sim task on EC2 (self-hosted runner)
378+ scheduler(
379+ action="add",
380+ job_id="sim_validation",
381+ cron="0 6 * * 1",
382+ prompt="Validate all robot models in Isaac Sim",
383+ workflow="isaac-sim.yml"
384+ )
385+
363386 # Schedule a one-time deployment for a specific time
364387 scheduler(
365388 action="add",
@@ -369,14 +392,6 @@ def scheduler(
369392 once=True # Auto-remove after execution
370393 )
371394
372- # Schedule a reminder (one-time, keeps in history)
373- scheduler(
374- action="add",
375- job_id="team_meeting_reminder",
376- run_at="2024-01-19T09:00:00Z",
377- prompt="Remind the team about the standup meeting"
378- )
379-
380395 # Check which jobs should run now (used by control loop)
381396 scheduler(action="check")
382397
@@ -409,28 +424,26 @@ def scheduler(
409424 Examples:
410425 - "2024-01-20T14:00:00Z" = January 20, 2024 at 2:00 PM UTC
411426 - "2024-01-20T09:30:00" = January 20, 2024 at 9:30 AM (assumes UTC)
427+
428+ Workflow Targets:
429+ - "agent.yml" (default) - Runs on ubuntu-latest (cloud agent)
430+ - "thor.yml" - Runs on self-hosted Thor (NVIDIA Jetson AGX Thor, GPU, CUDA)
431+ - "isaac-sim.yml" - Runs on self-hosted Isaac Sim EC2 (L40S GPU, Isaac Sim 5.1)
432+ - Any other workflow file that accepts workflow_dispatch with prompt input
412433 """
413434 try :
414435 repo = repository or _get_repository ()
415436 if not repo :
416437 return {
417438 "status" : "error" ,
418- "content" : [
419- {
420- "text" : "Error: Repository not specified and GITHUB_REPOSITORY not set"
421- }
422- ],
439+ "content" : [{"text" : "Error: Repository not specified and GITHUB_REPOSITORY not set" }],
423440 }
424441
425442 token = _get_github_token ()
426443 if not token :
427444 return {
428445 "status" : "error" ,
429- "content" : [
430- {
431- "text" : "Error: GitHub token not available (PAT_TOKEN or GITHUB_TOKEN)"
432- }
433- ],
446+ "content" : [{"text" : "Error: GitHub token not available (PAT_TOKEN or GITHUB_TOKEN)" }],
434447 }
435448
436449 # LIST - Show all jobs
@@ -451,7 +464,15 @@ def scheduler(
451464 for jid , job in jobs .items ():
452465 enabled = "✅" if job .get ("enabled" , True ) else "❌"
453466 job_type = "🔄" if job .get ("cron" ) else "📅"
454- lines .append (f"### { enabled } { job_type } `{ jid } `" )
467+ target_wf = job .get ("workflow" , "agent.yml" )
468+ runner_icons = {
469+ "agent.yml" : "🖥️" ,
470+ "thor.yml" : "🤖" ,
471+ "isaac-sim.yml" : "🎮" ,
472+ }
473+ runner_icon = runner_icons .get (target_wf , "⚙️" )
474+ lines .append (f"### { enabled } { job_type } { runner_icon } `{ jid } `" )
475+ lines .append (f"- **Workflow:** `{ target_wf } `" )
455476
456477 if job .get ("cron" ):
457478 lines .append (f"- **Cron:** `{ job ['cron' ]} ` (recurring)" )
@@ -509,6 +530,7 @@ def scheduler(
509530 "max_tokens" : job .get ("max_tokens" ),
510531 "context" : job .get ("context" ),
511532 "once" : job .get ("once" , False ),
533+ "workflow" : job .get ("workflow" , "agent.yml" ),
512534 }
513535 )
514536
@@ -521,11 +543,7 @@ def scheduler(
521543 if not jobs_to_run :
522544 return {
523545 "status" : "success" ,
524- "content" : [
525- {
526- "text" : f"No jobs scheduled to run at { now .strftime ('%Y-%m-%d %H:%M' )} UTC"
527- }
528- ],
546+ "content" : [{"text" : f"No jobs scheduled to run at { now .strftime ('%Y-%m-%d %H:%M' )} UTC" }],
529547 "jobs_to_run" : [],
530548 }
531549
@@ -541,9 +559,7 @@ def scheduler(
541559 lines .append ("" )
542560
543561 if jobs_to_remove :
544- lines .append (
545- f"\n *Removed { len (jobs_to_remove )} one-time job(s): { ', ' .join (jobs_to_remove )} *"
546- )
562+ lines .append (f"\n *Removed { len (jobs_to_remove )} one-time job(s): { ', ' .join (jobs_to_remove )} *" )
547563
548564 return {
549565 "status" : "success" ,
@@ -589,9 +605,7 @@ def scheduler(
589605 return {
590606 "status" : "error" ,
591607 "content" : [
592- {
593- "text" : f"Error: Invalid run_at format. Use ISO 8601 (e.g., 2024-01-20T14:00:00Z)"
594- }
608+ {"text" : "Error: Invalid run_at format. Use ISO 8601 (e.g., 2024-01-20T14:00:00Z)" }
595609 ],
596610 }
597611
@@ -624,6 +638,8 @@ def scheduler(
624638 schedules ["jobs" ][job_id ]["max_tokens" ] = max_tokens
625639 if context :
626640 schedules ["jobs" ][job_id ]["context" ] = context
641+ if workflow :
642+ schedules ["jobs" ][job_id ]["workflow" ] = workflow
627643
628644 result = _save_schedules (repo , schedules , token )
629645 if not result ["success" ]:
@@ -635,12 +651,18 @@ def scheduler(
635651 action_word = "updated" if is_update else "added"
636652 schedule_info = f"**Cron:** `{ cron } `" if cron else f"**Run At:** `{ run_at } `"
637653 once_info = " (once, auto-remove)" if once else ""
654+ workflow_info = f"\n **Workflow:** `{ workflow } `" if workflow else ""
655+ prompt_preview = prompt [:100 ]
638656
639657 return {
640658 "status" : "success" ,
641659 "content" : [
642660 {
643- "text" : f"✅ Job `{ job_id } ` { action_word } successfully\n \n { schedule_info } { once_info } \n **Prompt:** { prompt [:100 ]} ..."
661+ "text" : (
662+ f"✅ Job `{ job_id } ` { action_word } successfully\n \n "
663+ f"{ schedule_info } { once_info } { workflow_info } \n "
664+ f"**Prompt:** { prompt_preview } ..."
665+ )
644666 }
645667 ],
646668 }
@@ -727,8 +749,10 @@ def scheduler(
727749
728750 job = jobs [job_id ]
729751 job_type = "🔄 Recurring" if job .get ("cron" ) else "📅 One-time"
752+ target_wf = job .get ("workflow" , "agent.yml" )
730753 lines = [f"## Job: `{ job_id } ` ({ job_type } )\n " ]
731754 lines .append (f"**Enabled:** { '✅' if job .get ('enabled' , True ) else '❌' } " )
755+ lines .append (f"**Workflow:** `{ target_wf } `" )
732756
733757 if job .get ("cron" ):
734758 lines .append (f"**Cron:** `{ job ['cron' ]} `" )
@@ -758,9 +782,7 @@ def scheduler(
758782 return {
759783 "status" : "error" ,
760784 "content" : [
761- {
762- "text" : f"Unknown action: { action } . Valid: list, check, add, remove, enable, disable, get"
763- }
785+ {"text" : f"Unknown action: { action } . Valid: list, check, add, remove, enable, disable, get" }
764786 ],
765787 }
766788
0 commit comments