loopengine 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,7 @@
28
28
  import { devUiCss } from './dev-ui-styles.js';
29
29
  import { MESSAGING_ICONS } from './messaging-icons.js';
30
30
  import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
31
+ import { REASONING_PROVIDERS } from './agent-file-admin.js';
31
32
  export const agentsConfigPageHtml = `<!doctype html>
32
33
  <html lang="en">
33
34
  <head>
@@ -48,8 +49,20 @@ export const agentsConfigPageHtml = `<!doctype html>
48
49
  form.add-source .schedule-when input[type="number"] { width: 80px; }
49
50
  form.add-source .schedule-when select, form.add-source .schedule-when input[type="datetime-local"] { width: auto; }
50
51
  .workflow-card { margin-bottom: 10px; }
51
- .workflow-steps { margin: 0; padding-left: 20px; font-size: 13px; }
52
- .workflow-steps li { margin: 3px 0; }
52
+ .workflow-steps-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; margin: 6px 0 0; padding: 8px 10px; border-radius: 6px; background: var(--le-surface, #f6f7f9); }
53
+ .workflow-params { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
54
+ .workflow-params[hidden] { display: none; }
55
+ .workflow-editor[hidden] { display: none; }
56
+ .workflow-hooks[hidden] { display: none; }
57
+ .hook-custom[hidden] { display: none; }
58
+ .workflow-hook { border-top: 1px solid var(--le-line, #e5e7eb); padding: 8px 0; }
59
+ .hook-url { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; margin: 6px 0; border-radius: 6px; background: var(--le-surface, #f6f7f9); font-size: 13px; }
60
+ .hook-url input { font-family: var(--le-mono, monospace); font-size: 12.5px; }
61
+ .workflow-param-rows { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
62
+ .workflow-param-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
63
+ .workflow-param-row .wp-name { width: 160px; font-family: var(--le-mono, monospace); }
64
+ .workflow-param-row .wp-desc { flex: 1; min-width: 200px; }
65
+ .workflow-param { display: flex; flex-direction: column; gap: 3px; font-size: 12px; min-width: 180px; }
53
66
  .advanced-part { font-size: 17px; font-weight: 600; margin: 28px 0 12px; padding-top: 16px; border-top: 1px solid var(--line); }
54
67
  .advanced-part#advanced-tools { margin-top: 0; padding-top: 0; border-top: 0; }
55
68
  .approvals-nudge { border: 1px solid var(--warn); background: var(--warn-soft); color: var(--ink); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 24px; font-size: 13px; }
@@ -191,6 +204,9 @@ export const agentsConfigPageHtml = `<!doctype html>
191
204
  max-width: 480px;
192
205
  }
193
206
  form.add-source label { font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
207
+ form.add-source label[hidden] { display: none; }
208
+ .schedule-notify { display: flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 12px; cursor: pointer; }
209
+ .schedule-notify input { width: auto; margin: 0; }
194
210
  form.add-source input, form.add-source select, form.add-source textarea { width: 100%; }
195
211
  form.add-source textarea { font-family: ui-monospace, monospace; min-height: 60px; }
196
212
  /* Overrides form.add-source's shared max-width/textarea defaults for
@@ -358,6 +374,8 @@ ${consoleSidebarHtml}
358
374
  ${consoleShellScript}
359
375
  <script>
360
376
  (function () {
377
+ // The providers whose models take a reasoning effort (web/agent-file-admin.ts).
378
+ var REASONING_PROVIDERS = ${JSON.stringify(REASONING_PROVIDERS)};
361
379
  var empty = document.getElementById('empty');
362
380
  var detail = document.getElementById('detail');
363
381
  var currentName = null;
@@ -675,7 +693,7 @@ ${consoleShellScript}
675
693
  return workflows.map(function (w) {
676
694
  var s = w.schedule;
677
695
  var state = !s
678
- ? (w.trigger.kind === 'schedule' ? '<span class="hint">no schedule found</span>' : '<span class="hint">started when asked</span>')
696
+ ? '<span class="hint">' + escapeHtml(w.trigger) + '</span>'
679
697
  : s.waiting
680
698
  ? '<span class="le-pill ask">Waiting for a decision</span>'
681
699
  : s.status === 'paused'
@@ -683,53 +701,337 @@ ${consoleShellScript}
683
701
  : s.status === 'running'
684
702
  ? '<span class="le-pill accent">Running</span>'
685
703
  : '<span class="le-pill good">On</span>' + (s.nextRunAt ? ' <span class="hint">next ' + escapeHtml(new Date(s.nextRunAt).toLocaleString()) + '</span>' : '');
686
- var steps = (w.steps || []).map(function (st) {
687
- var mark = st.kind === 'ask' ? ' <span class="le-pill accent">Asks you</span>' : st.kind === 'approval' ? ' <span class="le-pill ask">You approve</span>' : '';
688
- return '<li>' + escapeHtml(st.title) + (st.tool && st.tool !== 'system_ask_user' ? ' <code>' + escapeHtml(st.tool) + '</code>' : '') + mark + '</li>';
704
+ var paramNames = Object.keys(w.parameters || {});
705
+ var params = paramNames.map(function (p) {
706
+ var spec = w.parameters[p];
707
+ return '<label class="workflow-param">' + escapeHtml(p) + (spec.required ? '' : ' <span class="hint">(optional)</span>') +
708
+ '<input name="' + escapeHtml(p) + '"' + (spec.required ? ' required' : '') + ' placeholder="' + escapeHtml(spec.description || '') + '"></label>';
709
+ }).join('');
710
+ var scheduleCount = (w.schedules || []).filter(function (x) { return x.repeating; }).length;
711
+ var runs = (w.recentRuns || []).map(function (r) {
712
+ var pill = r.status === 'done' ? '<span class="le-pill good">Done</span>'
713
+ : r.status === 'error' ? '<span class="le-pill bad">Failed</span>'
714
+ : r.status === 'awaiting_approval' ? '<span class="le-pill ask">Waiting for a decision</span>'
715
+ : '<span class="le-pill accent">Running</span>';
716
+ var values = r.params ? Object.keys(r.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(r.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '';
717
+ var outcome = r.status === 'error' ? (r.error || '') : (r.result || '');
718
+ return '<li>' + pill + ' <span class="hint">' + escapeHtml(new Date(r.started_at).toLocaleString()) + (r.source === 'api' ? ' ' + String.fromCharCode(183) + ' from an app' : r.source === 'schedule' ? ' ' + String.fromCharCode(183) + ' scheduled' + (r.task_name ? ' (' + escapeHtml(r.task_name) + ')' : '') : '') + '</span>' +
719
+ (values ? '<div class="hint">' + values + '</div>' : '') +
720
+ (outcome ? '<div class="schedule-result">' + escapeHtml(outcome) + '</div>' : '') + '</li>';
689
721
  }).join('');
690
722
  return '<div class="source workflow-card" data-workflow="' + escapeHtml(w.id) + '">' +
691
723
  '<div style="display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:baseline">' +
692
- '<strong>' + escapeHtml(w.name) + '</strong><span>' + state + '</span></div>' +
693
- '<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.summary) + ' ' + String.fromCharCode(183) + ' ' + escapeHtml(w.trigger.description) + '</p>' +
694
- '<ol class="workflow-steps">' + steps + '</ol>' +
724
+ '<strong>' + escapeHtml(w.title) + '</strong><span>' + state + '</span></div>' +
725
+ '<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.description) + (s ? ' ' + String.fromCharCode(183) + ' ' + escapeHtml(w.trigger) : '') + '</p>' +
726
+ '<details><summary class="hint">Steps <span>(the ' + escapeHtml(w.id) + ' skill)</span></summary><div class="workflow-steps-text">' + escapeHtml(w.steps) + '</div></details>' +
727
+ (params ? '<div class="workflow-params">' + params + '</div>' : '') +
728
+ (runs ? '<details class="workflow-runs"' + ((w.recentRuns || []).some(function (r) { return r.status === 'running' || r.status === 'awaiting_approval'; }) ? ' open' : '') + '><summary class="hint">Recent runs (' + w.recentRuns.length + ')</summary><ul class="schedule-history">' + runs + '</ul></details>' : '') +
695
729
  '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">' +
696
730
  '<button type="button" class="workflow-run">Run now</button>' +
697
- (s ? '<a href="?tab=schedules" data-tab="schedules" class="hint" style="align-self:center">Schedule</a>' : '') +
731
+ (scheduleCount ? '<a href="?tab=schedules" data-tab="schedules" class="hint" style="align-self:center">' + (scheduleCount > 1 ? 'Schedules (' + scheduleCount + ')' : 'Schedule') + '</a>' : '') +
732
+ '<button type="button" class="workflow-edit">Edit</button>' +
733
+ '<button type="button" class="workflow-hooks-btn">Webhooks</button>' +
734
+ '<a href="?tab=skills" data-tab="skills" class="hint" style="align-self:center">Edit steps</a>' +
698
735
  '<button type="button" class="delete-btn workflow-forget">Forget</button>' +
699
- '</div><div class="hint workflow-result"></div></div>';
736
+ '</div><div class="hint workflow-result"></div><div class="workflow-editor" hidden></div><div class="workflow-hooks" hidden></div></div>';
700
737
  }).join('') + '<p class="hint" style="margin:6px 0 0"><a href="/workflows/new">Describe another workflow</a></p>';
701
738
  }
702
739
 
740
+ var workflowsRefreshTimer = null;
741
+ var workflowsById = {};
742
+ // Set while a workflow's editor is open, so the running-run refresh
743
+ // doesn't redraw the card under someone typing.
744
+ var workflowEditing = false;
745
+
703
746
  function loadWorkflowsSection(name) {
704
747
  var el = detail.querySelector('#workflowsContent');
705
748
  if (!el) return;
749
+ if (workflowsRefreshTimer) { clearTimeout(workflowsRefreshTimer); workflowsRefreshTimer = null; }
706
750
  fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows').then(function (r) {
707
751
  if (currentName !== name) return;
708
752
  if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
709
- el.innerHTML = renderWorkflowsHtml(name, r.body.workflows || []);
753
+ var workflows = r.body.workflows || [];
754
+ workflowsById = {};
755
+ workflows.forEach(function (w) { workflowsById[w.id] = w; });
756
+ el.innerHTML = renderWorkflowsHtml(name, workflows);
710
757
  var cards = el.querySelectorAll('.workflow-card');
711
758
  for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
759
+ // Keep a running run's status current while the Overview is open.
760
+ var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
761
+ if (running && currentTab === 'overview') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
712
762
  });
713
763
  }
714
764
 
765
+ // ---- The workflow editor: how a workflow's runs start. Its steps are
766
+ // the skill body, edited on the Skills tab.
767
+ function workflowParamRowHtml(pname, spec) {
768
+ return '<div class="workflow-param-row">' +
769
+ '<input class="wp-name" aria-label="Parameter name" placeholder="order_number" value="' + escapeHtml(pname || '') + '">' +
770
+ '<input class="wp-desc" aria-label="What it is" placeholder="What it is, e.g. The order to check" value="' + escapeHtml((spec && spec.description) || '') + '">' +
771
+ '<label class="http-tool-inline-label"><input type="checkbox" class="wp-required"' + (spec && spec.required ? ' checked' : '') + '> Required</label>' +
772
+ '<button type="button" class="delete-btn wp-remove">Remove</button></div>';
773
+ }
774
+
775
+ function workflowEditorHtml(w) {
776
+ var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
777
+ return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
778
+ '<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '"></label>' +
779
+ '<label>Start message <span class="hint">(what each run begins with; $name puts in a parameter&#39;s value)</span>' +
780
+ '<textarea name="start_message" maxlength="4000" required>' + escapeHtml(w.startMessage) + '</textarea></label>' +
781
+ '<label>When it runs <span class="hint">(in words, for people; the timing itself is its schedule)</span><input name="trigger" maxlength="200" value="' + escapeHtml(w.trigger) + '"></label>' +
782
+ '<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
783
+ '<div class="workflow-param-rows">' + rows + '</div>' +
784
+ '<button type="button" class="wp-add" style="align-self:flex-start">Add parameter</button></div>' +
785
+ '<p class="hint workflow-editor-check" style="margin:0"></p>' +
786
+ '<div style="display:flex;gap:8px"><button type="submit" class="primary">Save</button><button type="button" class="wp-cancel">Cancel</button></div>' +
787
+ '<div class="hint workflow-editor-result"></div></form>';
788
+ }
789
+
790
+ // Placeholders used in the start message or steps that no parameter
791
+ // declares, and declared parameters nothing uses.
792
+ function workflowPlaceholderCheck(w, form) {
793
+ var used = {};
794
+ var pattern = new RegExp('[$]([a-z_][a-z0-9_]*)', 'g');
795
+ var text = form.querySelector('[name="start_message"]').value + ' ' + (w.steps || '');
796
+ var m;
797
+ while ((m = pattern.exec(text))) used[m[1]] = true;
798
+ var declared = {};
799
+ var names = form.querySelectorAll('.wp-name');
800
+ for (var i = 0; i < names.length; i++) if (names[i].value.trim()) declared[names[i].value.trim()] = true;
801
+ var notes = [];
802
+ var missing = Object.keys(used).filter(function (n) { return !declared[n]; });
803
+ var unused = Object.keys(declared).filter(function (n) { return !used[n]; });
804
+ if (missing.length) notes.push('Uses ' + missing.map(function (n) { return '$' + n; }).join(', ') + ', which ' + (missing.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't a parameter: add ' + (missing.length > 1 ? 'them' : 'it') + ', or the text stays as written.');
805
+ if (unused.length) notes.push(unused.join(', ') + ' ' + (unused.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't used in the start message or steps: its value is passed as an input.');
806
+ form.querySelector('.workflow-editor-check').textContent = notes.join(' ');
807
+ }
808
+
809
+ function openWorkflowEditor(name, card) {
810
+ var w = workflowsById[card.dataset.workflow];
811
+ var box = card.querySelector('.workflow-editor');
812
+ if (!w || !box) return;
813
+ workflowEditing = true;
814
+ box.innerHTML = workflowEditorHtml(w);
815
+ box.hidden = false;
816
+ var form = box.querySelector('form');
817
+ var rows = form.querySelector('.workflow-param-rows');
818
+ var result = form.querySelector('.workflow-editor-result');
819
+ function check() { workflowPlaceholderCheck(w, form); }
820
+ function close() { workflowEditing = false; box.hidden = true; box.innerHTML = ''; }
821
+ rows.addEventListener('click', function (ev) {
822
+ if (ev.target.classList.contains('wp-remove')) { ev.target.closest('.workflow-param-row').remove(); check(); }
823
+ });
824
+ form.addEventListener('input', check);
825
+ form.querySelector('.wp-add').addEventListener('click', function () {
826
+ rows.insertAdjacentHTML('beforeend', workflowParamRowHtml('', null));
827
+ rows.lastElementChild.querySelector('.wp-name').focus();
828
+ });
829
+ form.querySelector('.wp-cancel').addEventListener('click', close);
830
+ form.addEventListener('submit', function (ev) {
831
+ ev.preventDefault();
832
+ var parameters = {};
833
+ var problem = '';
834
+ var list = rows.querySelectorAll('.workflow-param-row');
835
+ for (var i = 0; i < list.length; i++) {
836
+ var pname = list[i].querySelector('.wp-name').value.trim();
837
+ if (!pname) continue;
838
+ if (!/^[a-z_][a-z0-9_]*$/.test(pname)) { problem = 'Parameter "' + pname + '": use lowercase letters, digits and _, starting with a letter, e.g. order_number.'; break; }
839
+ if (parameters[pname]) { problem = 'Parameter "' + pname + '" is listed twice.'; break; }
840
+ parameters[pname] = { description: list[i].querySelector('.wp-desc').value.trim(), required: list[i].querySelector('.wp-required').checked };
841
+ }
842
+ if (problem) { result.textContent = problem; return; }
843
+ result.textContent = 'Saving' + String.fromCharCode(8230);
844
+ fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
845
+ method: 'PUT', headers: { 'content-type': 'application/json' },
846
+ body: JSON.stringify({ title: form.title.value, start_message: form.start_message.value, trigger: form.trigger.value, parameters: parameters }),
847
+ }).then(function (r) {
848
+ if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
849
+ workflowEditing = false;
850
+ loadWorkflowsSection(name);
851
+ });
852
+ });
853
+ check();
854
+ form.title.focus();
855
+ }
856
+
857
+ // ---- A workflow's webhook triggers (core/workflow-triggers.ts): secret
858
+ // URLs an outside service posts its events to, to start a run.
859
+ var HOOK_OUTCOMES = {
860
+ started: ['good', 'Started a run'], filtered: ['', 'Skipped by the filter'], duplicate: ['', 'A retry, skipped'],
861
+ busy: ['', 'Already running'], rejected: ['bad', 'Signature check failed'], invalid: ['bad', 'Couldn' + String.fromCharCode(8217) + 't start'], error: ['bad', 'Failed'],
862
+ };
863
+ var HOOK_SENDERS = { stripe: 'Stripe', github: 'GitHub', shopify: 'Shopify', standard: 'Standard Webhooks' };
864
+
865
+ function hookRowHtml(t) {
866
+ var last = (t.deliveries || []).slice(0, 3).map(function (d) {
867
+ var o = HOOK_OUTCOMES[d.outcome] || ['', d.outcome];
868
+ return '<li><span class="le-pill ' + o[0] + '">' + escapeHtml(o[1]) + '</span> <span class="hint">' + escapeHtml(new Date(d.at).toLocaleString()) + '</span>' +
869
+ (d.detail ? '<div class="schedule-result">' + escapeHtml(d.detail) + '</div>' : '') + '</li>';
870
+ }).join('');
871
+ var rules = [];
872
+ if (t.verify) rules.push(t.verify.sender === 'custom'
873
+ ? 'checks the signature in ' + escapeHtml(t.verify.header) + ' (' + escapeHtml(t.verify.algorithm || 'sha256') + ', ' + escapeHtml(t.verify.encoding || 'hex') + ')'
874
+ : 'checks the ' + escapeHtml(HOOK_SENDERS[t.verify.sender] || t.verify.sender) + ' signature');
875
+ if (t.filter) rules.push('only when ' + escapeHtml(t.filter.path) + ' is ' + t.filter.values.map(escapeHtml).join(' or '));
876
+ if (t.mapping) rules.push(Object.keys(t.mapping).map(function (p) { return escapeHtml(p) + ' ' + String.fromCharCode(8592) + ' ' + escapeHtml(t.mapping[p]); }).join(', '));
877
+ if (t.payload_param) rules.push('the whole payload goes to ' + escapeHtml(t.payload_param));
878
+ if (t.notify) rules.push('sends each result to you');
879
+ if (t.callback_url) rules.push('reports to ' + escapeHtml(t.callback_url));
880
+ return '<div class="workflow-hook" data-trigger="' + escapeHtml(t.trigger_id) + '">' +
881
+ '<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:baseline"><strong>' + escapeHtml(t.name) + '</strong>' +
882
+ '<code class="hint">/hooks/workflows/' + escapeHtml(t.token_hint) + '</code>' + (t.enabled ? '' : ' <span class="le-pill">Off</span>') + '</div>' +
883
+ (rules.length ? '<p class="hint" style="margin:2px 0">' + rules.join(' ' + String.fromCharCode(183) + ' ') + '</p>' : '') +
884
+ (last ? '<ul class="schedule-history">' + last + '</ul>' : '<p class="hint" style="margin:2px 0">No deliveries yet.</p>') +
885
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px">' +
886
+ '<button type="button" class="hook-toggle">' + (t.enabled ? 'Turn off' : 'Turn on') + '</button>' +
887
+ '<button type="button" class="hook-rotate">New URL</button>' +
888
+ '<button type="button" class="delete-btn hook-delete">Delete</button></div></div>';
889
+ }
890
+
891
+ function hookFormHtml(w) {
892
+ var params = Object.keys(w.parameters || {});
893
+ return '<form class="add-source hook-form" style="margin-top:10px">' +
894
+ '<h4 style="margin:0">Add a webhook</h4>' +
895
+ '<label>Name<input name="name" maxlength="80" placeholder="Stripe disputes"></label>' +
896
+ '<label>Check the sender' + String.fromCharCode(8217) + 's signature <span class="hint">(the URL itself is secret; this also proves who sent it)</span>' +
897
+ '<select name="sender"><option value="">No, the URL is enough</option><option value="stripe">Stripe</option><option value="github">GitHub</option><option value="shopify">Shopify</option>' +
898
+ '<option value="standard">Standard Webhooks (Clerk, Resend, OpenAI, Svix)</option><option value="custom">Another service: set it up myself</option></select></label>' +
899
+ '<div class="hook-custom body-field" hidden>How it signs <span class="hint">(from its webhook docs: an HMAC of the request body, in a header)</span>' +
900
+ '<div class="workflow-param-row">' +
901
+ '<input name="sig_header" placeholder="Header, e.g. Linear-Signature" style="flex:1">' +
902
+ '<select name="sig_algorithm" aria-label="Hash"><option value="sha256">SHA-256</option><option value="sha1">SHA-1</option><option value="sha512">SHA-512</option></select>' +
903
+ '<select name="sig_encoding" aria-label="Written as"><option value="hex">hex</option><option value="base64">base64</option></select>' +
904
+ '<input name="sig_prefix" placeholder="Prefix, e.g. sha256= (if any)" style="width:180px">' +
905
+ '</div></div>' +
906
+ '<label class="hook-secret" hidden>Signing secret <span class="hint">(from the sender' + String.fromCharCode(8217) + 's webhook settings)</span><input name="secret" autocomplete="off"></label>' +
907
+ '<label>Run only when <span class="hint">(optional: a field in the payload, and the values that start a run)</span>' +
908
+ '<div class="workflow-param-row"><input name="filter_path" placeholder="type"><input name="filter_values" placeholder="charge.dispute.created, charge.dispute.updated" style="flex:1"></div></label>' +
909
+ (params.length
910
+ ? '<div class="body-field">Parameters from the payload <span class="hint">(a dot path into its JSON, e.g. data.object.id)</span>' +
911
+ params.map(function (p) { return '<div class="workflow-param-row"><code class="wp-name">' + escapeHtml(p) + '</code><input name="map:' + escapeHtml(p) + '" placeholder="data.object.id" style="flex:1"></div>'; }).join('') + '</div>' +
912
+ '<label>Or give the whole payload to <select name="payload_param"><option value="">(none)</option>' + params.map(function (p) { return '<option>' + escapeHtml(p) + '</option>'; }).join('') + '</select></label>'
913
+ : '<p class="hint" style="margin:0">This workflow takes no parameters, so the payload isn' + String.fromCharCode(8217) + 't passed on. Add one in Edit to use it.</p>') +
914
+ '<label class="http-tool-inline-label"><input type="checkbox" name="notify"> Send each run' + String.fromCharCode(8217) + 's result to you through the agent' + String.fromCharCode(8217) + 's messaging app</label>' +
915
+ '<label>Post each run' + String.fromCharCode(8217) + 's result to <span class="hint">(optional, an https URL)</span><input name="callback_url" placeholder="https://example.com/hooks/loopengine"></label>' +
916
+ '<div style="display:flex;gap:8px"><button type="submit" class="primary">Make the URL</button></div>' +
917
+ '<div class="hint hook-form-result"></div></form>';
918
+ }
919
+
920
+ function hookUrlHtml(url) {
921
+ return '<div class="hook-url"><strong>Copy this URL now</strong> <span class="hint">(it' + String.fromCharCode(8217) + 's shown only once; make a new one if it' + String.fromCharCode(8217) + 's lost)</span>' +
922
+ '<input readonly value="' + escapeHtml(url) + '" onclick="this.select()"></div>';
923
+ }
924
+
925
+ function openWorkflowHooks(name, card) {
926
+ var w = workflowsById[card.dataset.workflow];
927
+ var box = card.querySelector('.workflow-hooks');
928
+ if (!w || !box) return;
929
+ if (!box.hidden) { box.hidden = true; box.innerHTML = ''; workflowEditing = false; return; }
930
+ workflowEditing = true;
931
+ box.hidden = false;
932
+ var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id) + '/triggers';
933
+ function render(shownUrl) {
934
+ box.innerHTML = '<p class="hint" style="margin:0">Loading' + String.fromCharCode(8230) + '</p>';
935
+ fetchJsonRetryOnce(base).then(function (r) {
936
+ if (!r.ok) { box.innerHTML = '<p class="error">' + escapeHtml(r.body.error || 'Could not load webhooks') + '</p>'; return; }
937
+ var list = r.body.triggers || [];
938
+ box.innerHTML = '<h4 style="margin:10px 0 4px">Webhooks</h4>' +
939
+ '<p class="hint" style="margin:0 0 6px">A secret URL an outside service (Stripe, GitHub, a form, monitoring) posts its events to; each one starts a run.</p>' +
940
+ (shownUrl ? hookUrlHtml(shownUrl) : '') +
941
+ (list.length ? list.map(hookRowHtml).join('') : '') + hookFormHtml(w);
942
+ wire();
943
+ });
944
+ }
945
+ function act(id, path, opts) {
946
+ return fetchJsonRetryOnce(base + '/' + encodeURIComponent(id) + path, opts).then(function (r) {
947
+ if (!r.ok) { alert(r.body.error || 'That didn' + String.fromCharCode(8217) + 't work'); return; }
948
+ render(r.body.url);
949
+ });
950
+ }
951
+ function wire() {
952
+ var rows = box.querySelectorAll('.workflow-hook');
953
+ for (var i = 0; i < rows.length; i++) {
954
+ (function (row) {
955
+ var id = row.dataset.trigger;
956
+ row.querySelector('.hook-toggle').addEventListener('click', function (ev) {
957
+ act(id, '', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled: ev.currentTarget.textContent === 'Turn on' }) });
958
+ });
959
+ row.querySelector('.hook-rotate').addEventListener('click', function (ev) {
960
+ var btn = ev.currentTarget;
961
+ if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again: the old URL stops working'; return; }
962
+ act(id, '/rotate', { method: 'POST' });
963
+ });
964
+ row.querySelector('.hook-delete').addEventListener('click', function (ev) {
965
+ var btn = ev.currentTarget;
966
+ if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to delete'; return; }
967
+ act(id, '', { method: 'DELETE' });
968
+ });
969
+ })(rows[i]);
970
+ }
971
+ var form = box.querySelector('.hook-form');
972
+ var secretField = form.querySelector('.hook-secret');
973
+ var customField = form.querySelector('.hook-custom');
974
+ form.sender.addEventListener('change', function () {
975
+ secretField.hidden = !form.sender.value;
976
+ customField.hidden = form.sender.value !== 'custom';
977
+ });
978
+ form.addEventListener('submit', function (ev) {
979
+ ev.preventDefault();
980
+ var result = form.querySelector('.hook-form-result');
981
+ var body = { name: form.name.value };
982
+ if (form.sender.value) body.verify = { sender: form.sender.value, secret: form.secret.value };
983
+ if (form.sender.value === 'custom') {
984
+ body.verify.header = form.sig_header.value.trim();
985
+ body.verify.algorithm = form.sig_algorithm.value;
986
+ body.verify.encoding = form.sig_encoding.value;
987
+ if (form.sig_prefix.value.trim()) body.verify.prefix = form.sig_prefix.value.trim();
988
+ }
989
+ var values = form.filter_values.value.split(',').map(function (v) { return v.trim(); }).filter(Boolean);
990
+ if (form.filter_path.value.trim() || values.length) body.filter = { path: form.filter_path.value.trim(), values: values };
991
+ var mapping = {};
992
+ var inputs = form.querySelectorAll('input[name^="map:"]');
993
+ for (var j = 0; j < inputs.length; j++) if (inputs[j].value.trim()) mapping[inputs[j].name.slice(4)] = inputs[j].value.trim();
994
+ if (Object.keys(mapping).length) body.mapping = mapping;
995
+ if (form.payload_param && form.payload_param.value) body.payload_param = form.payload_param.value;
996
+ if (form.notify.checked) body.notify = true;
997
+ if (form.callback_url.value.trim()) body.callback_url = form.callback_url.value.trim();
998
+ result.textContent = 'Making it' + String.fromCharCode(8230);
999
+ fetchJsonRetryOnce(base, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) {
1000
+ if (!r.ok) { result.textContent = r.body.error || 'Could not make it'; return; }
1001
+ render(r.body.url);
1002
+ });
1003
+ });
1004
+ }
1005
+ render(null);
1006
+ }
1007
+
715
1008
  function wireWorkflowCard(name, card) {
716
1009
  var id = card.dataset.workflow;
717
1010
  var result = card.querySelector('.workflow-result');
718
1011
  var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(id);
1012
+ card.querySelector('.workflow-edit').addEventListener('click', function () { openWorkflowEditor(name, card); });
1013
+ card.querySelector('.workflow-hooks-btn').addEventListener('click', function () { openWorkflowHooks(name, card); });
719
1014
  card.querySelector('.workflow-run').addEventListener('click', function (ev) {
720
1015
  var btn = ev.currentTarget;
1016
+ var params = {};
1017
+ var inputs = card.querySelectorAll('.workflow-params input');
1018
+ for (var i = 0; i < inputs.length; i++) {
1019
+ var value = inputs[i].value.trim();
1020
+ if (!value && inputs[i].required) { result.textContent = 'Fill in ' + inputs[i].name + ' first.'; inputs[i].focus(); return; }
1021
+ if (value) params[inputs[i].name] = value;
1022
+ }
721
1023
  btn.disabled = true;
722
1024
  result.textContent = 'Starting' + String.fromCharCode(8230);
723
- fetchJsonRetryOnce(base + '/run', { method: 'POST' }).then(function (r) {
1025
+ fetchJsonRetryOnce(base + '/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ params: params, source: 'console' }) }).then(function (r) {
724
1026
  btn.disabled = false;
725
- result.textContent = r.ok
726
- ? 'Started. Questions and approvals go to this agent' + String.fromCharCode(8217) + 's messaging app; its result shows in the Schedules tab.'
727
- : (r.body.error || 'Could not start it.');
1027
+ if (!r.ok) { result.textContent = r.body.error || 'Could not start it.'; return; }
1028
+ result.textContent = 'Started. Questions and approvals go to this agent' + String.fromCharCode(8217) + 's messaging app; the result shows under Recent runs.';
1029
+ setTimeout(function () { loadWorkflowsSection(name); }, 800);
728
1030
  });
729
1031
  });
730
1032
  card.querySelector('.workflow-forget').addEventListener('click', function (ev) {
731
1033
  var btn = ev.currentTarget;
732
- if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its skill, schedule and rules stay)'; return; }
1034
+ if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its schedules are cancelled; the skill stays)'; return; }
733
1035
  fetchJsonRetryOnce(base, { method: 'DELETE' }).then(function () { loadWorkflowsSection(name); });
734
1036
  });
735
1037
  }
@@ -792,7 +1094,7 @@ ${consoleShellScript}
792
1094
  '<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
793
1095
  '<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
794
1096
  '<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
795
- '<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (cfg.model.provider === 'openai' ? '(provider default)' : 'n/a')) + '</dd>' +
1097
+ '<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (REASONING_PROVIDERS.indexOf(cfg.model.provider) !== -1 ? '(model default)' : 'n/a')) + '</dd>' +
796
1098
  '</dl>' +
797
1099
  '<form class="add-source" id="modelForm" style="display:none">' +
798
1100
  '<label>Provider' +
@@ -808,7 +1110,7 @@ ${consoleShellScript}
808
1110
  '<label>Max tokens <span class="hint">(blank for provider default)</span>' +
809
1111
  '<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
810
1112
  '</label>' +
811
- '<label>Reasoning effort <span class="hint">(openai only — some reasoning models reject function tools unless this is \\\'none\\\'; leave unset otherwise)</span>' +
1113
+ '<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default)</span>' +
812
1114
  '<select name="reasoningEffort">' +
813
1115
  ['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
814
1116
  return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
@@ -918,7 +1220,19 @@ ${consoleShellScript}
918
1220
  var modelForm = overviewPanel.querySelector('#modelForm');
919
1221
  var modelError = overviewPanel.querySelector('#modelError');
920
1222
 
1223
+ // Only OpenAI and Anthropic models take a reasoning effort: the
1224
+ // field shows for those, and is cleared for any other provider so
1225
+ // it's never saved with one.
1226
+ var providerSelect = modelForm.querySelector('select[name="provider"]');
1227
+ var reasoningField = modelForm.querySelector('#reasoningEffortField');
1228
+ function syncReasoningField() {
1229
+ var supported = REASONING_PROVIDERS.indexOf(providerSelect.value) !== -1;
1230
+ reasoningField.style.display = supported ? '' : 'none';
1231
+ if (!supported) reasoningField.querySelector('select').value = '';
1232
+ }
1233
+ providerSelect.addEventListener('change', syncReasoningField);
921
1234
  editModelBtn.addEventListener('click', function () {
1235
+ syncReasoningField();
922
1236
  modelDisplay.style.display = 'none';
923
1237
  editModelBtn.style.display = 'none';
924
1238
  modelForm.style.display = '';
@@ -1760,6 +2074,9 @@ ${consoleShellScript}
1760
2074
  // The workspace's time zone, from GET /agents/:name/schedules (an
1761
2075
  // older project adapter doesn't send it: then the browser's own).
1762
2076
  var schedulesTimeZone = null;
2077
+ // The agent's workflows, from the same response: a new schedule can run
2078
+ // one of them instead of a message.
2079
+ var schedulesWorkflows = [];
1763
2080
 
1764
2081
  function fmtScheduleTime(iso) {
1765
2082
  var d = new Date(iso);
@@ -1792,8 +2109,16 @@ ${consoleShellScript}
1792
2109
  '<div class="schedule-result">' + escapeHtml(h.status === 'ok' ? (h.result || '') : (h.error || '')) + '</div></li>';
1793
2110
  }).join('');
1794
2111
  return '<tr>' +
1795
- '<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' + (t.notify ? ' <span class="le-pill accent" title="Each result goes through this agent&#39;s notifier">notifies</span>' : '') +
1796
- '<div class="schedule-result hint">' + escapeHtml(t.message) + '</div></td>' +
2112
+ '<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
2113
+ (t.workflow
2114
+ ? '<div><span class="le-pill accent">Workflow</span> ' + escapeHtml(t.workflow.title) + '</div><div class="schedule-result hint">' + escapeHtml(t.workflow.start_message) +
2115
+ (t.params ? '<br>' + Object.keys(t.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(t.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '') + '</div>'
2116
+ : t.workflow_missing
2117
+ ? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
2118
+ : '<div class="schedule-result hint">' + escapeHtml(t.message) + '</div>') +
2119
+ (t.status !== 'cancelled' && t.status !== 'done'
2120
+ ? '<label class="schedule-notify hint" title="Send each run&#39;s result through this agent&#39;s messaging connection (Messaging tab)"><input type="checkbox" class="schedule-notify-toggle" data-id="' + escapeHtml(t.task_id) + '"' + (t.notify ? ' checked' : '') + '> Send each result to you</label>'
2121
+ : (t.notify ? '<div class="hint">Sent each result to you</div>' : '')) + '</td>' +
1797
2122
  '<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(fmtScheduleTime(t.next_run_at)) + '</div>' : '') + '</td>' +
1798
2123
  '<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
1799
2124
  (t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
@@ -1808,11 +2133,22 @@ ${consoleShellScript}
1808
2133
  var list = tasks.length
1809
2134
  ? '<div class="le-table-wrap"><table><thead><tr><th>Task</th><th>When</th><th>Status</th><th>Last run</th><th></th></tr></thead><tbody>' + tasks.map(renderScheduleRow).join('') + '</tbody></table></div>'
1810
2135
  : '<p class="hint">Nothing scheduled yet.</p>';
1811
- return '<p class="muted" style="margin:0 0 20px">Messages this agent receives on a timer, like a cron job. Each run works like an app calling it over HTTP: an approval goes wherever the Messaging tab sends it, and with nowhere set, a call that needs approval is denied. The agent can also schedule its own tasks when asked in Chat (&ldquo;remind me tomorrow at 9&rdquo;); they appear here too.</p>' +
2136
+ return '<p class="muted" style="margin:0 0 20px">Workflows and messages this agent runs on a timer, like a cron job. A workflow schedule starts with the workflow&#39;s start message as it is at each run. Each run works like an app calling it over HTTP: an approval goes wherever the Messaging tab sends it, and with nowhere set, a call that needs approval is denied. The agent can also schedule its own tasks when asked in Chat (&ldquo;remind me tomorrow at 9&rdquo;); they appear here too.</p>' +
1812
2137
  '<section><h3>Scheduled (' + active.length + ' active)</h3>' + list + '</section>' +
1813
2138
  '<section><h3>New schedule</h3>' +
1814
2139
  '<form class="add-source" id="scheduleForm" style="max-width:560px">' +
1815
- '<label>Message <span class="hint">(what the agent receives each time, as if someone typed it)</span><textarea name="message" required placeholder="Summarize yesterday&#39;s support tickets and post the summary."></textarea></label>' +
2140
+ (schedulesWorkflows.length
2141
+ ? '<div class="body-field">Runs' +
2142
+ '<div class="schedule-when"><input type="radio" name="runs" value="workflow" id="runsWorkflow" checked><label for="runsWorkflow">A workflow</label>' +
2143
+ '<select name="workflow_id" aria-label="Workflow">' + schedulesWorkflows.map(function (w) {
2144
+ return '<option value="' + escapeHtml(w.id) + '">' + escapeHtml(w.title) + '</option>';
2145
+ }).join('') + '</select></div>' +
2146
+ '<div class="workflow-params" id="scheduleParams" style="margin:0 0 4px 22px"></div>' +
2147
+ '<p class="hint" style="margin:0 0 4px 22px">Each run starts with the workflow&#39;s start message as it is then, so changing the workflow changes what runs.</p>' +
2148
+ '<div class="schedule-when"><input type="radio" name="runs" value="message" id="runsMessage"><label for="runsMessage">A message</label></div>' +
2149
+ '</div>'
2150
+ : '') +
2151
+ '<label id="scheduleMessageField"' + (schedulesWorkflows.length ? ' hidden' : '') + '>Message <span class="hint">(what the agent receives each time, as if someone typed it)</span><textarea name="message" placeholder="Summarize yesterday&#39;s support tickets and post the summary."></textarea></label>' +
1816
2152
  '<div class="body-field">When' +
1817
2153
  '<div class="schedule-when"><input type="radio" name="when" value="every" id="whenEvery" checked><label for="whenEvery">Every</label>' +
1818
2154
  '<input type="number" name="everyN" value="1" min="1" aria-label="How many">' +
@@ -1830,6 +2166,15 @@ ${consoleShellScript}
1830
2166
  '</form></section>';
1831
2167
  }
1832
2168
 
2169
+ // The New schedule form's parameter fields ("param:<name>"), filled ones only.
2170
+ function scheduleParams(data) {
2171
+ var params = {};
2172
+ data.forEach(function (value, key) {
2173
+ if (key.indexOf('param:') === 0 && String(value).trim()) params[key.slice(6)] = String(value).trim();
2174
+ });
2175
+ return params;
2176
+ }
2177
+
1833
2178
  // A date field's value ("2026-11-02T09:00") goes as it is, for the
1834
2179
  // server to read in the workspace's time zone; with no workspace time
1835
2180
  // zone known, it's the browser's.
@@ -1857,15 +2202,53 @@ ${consoleShellScript}
1857
2202
  request.then(function () { setTimeout(function () { loadSchedulesTab(name); }, action === 'run' ? 800 : 0); });
1858
2203
  });
1859
2204
  }
2205
+ var notifyToggles = content.querySelectorAll('.schedule-notify-toggle');
2206
+ for (var n = 0; n < notifyToggles.length; n++) {
2207
+ notifyToggles[n].addEventListener('change', function (ev) {
2208
+ var box = ev.currentTarget;
2209
+ box.disabled = true;
2210
+ fetchJsonRetryOnce('/schedules/' + encodeURIComponent(box.dataset.id) + '/notify', {
2211
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ notify: box.checked }),
2212
+ }).then(function (r) {
2213
+ box.disabled = false;
2214
+ if (!r.ok) { box.checked = !box.checked; alert('Could not change it: ' + (r.body.error || 'unknown error')); }
2215
+ });
2216
+ });
2217
+ }
1860
2218
  var toApprovals = content.querySelectorAll('.schedule-to-approvals');
1861
2219
  for (var j = 0; j < toApprovals.length; j++) {
1862
2220
  toApprovals[j].addEventListener('click', function (ev) { ev.preventDefault(); switchTab('messaging'); });
1863
2221
  }
1864
2222
  var form = content.querySelector('#scheduleForm');
1865
2223
  var result = content.querySelector('#scheduleResult');
2224
+ var messageField = content.querySelector('#scheduleMessageField');
2225
+ var runsRadios = content.querySelectorAll('input[name="runs"]');
2226
+ var paramsBox = content.querySelector('#scheduleParams');
2227
+ var workflowSelect = form.querySelector('select[name="workflow_id"]');
2228
+ // One field per parameter of the chosen workflow.
2229
+ function renderScheduleParams() {
2230
+ if (!paramsBox || !workflowSelect) return;
2231
+ var w = schedulesWorkflows.filter(function (x) { return x.id === workflowSelect.value; })[0];
2232
+ var parameters = (w && w.parameters) || {};
2233
+ paramsBox.innerHTML = Object.keys(parameters).map(function (p) {
2234
+ return '<label class="workflow-param">' + escapeHtml(p) + (parameters[p].required ? '' : ' <span class="hint">(optional)</span>') +
2235
+ '<input name="param:' + escapeHtml(p) + '" placeholder="' + escapeHtml(parameters[p].description || '') + '"></label>';
2236
+ }).join('');
2237
+ }
2238
+ renderScheduleParams();
2239
+ if (workflowSelect) workflowSelect.addEventListener('change', renderScheduleParams);
2240
+ for (var k = 0; k < runsRadios.length; k++) {
2241
+ runsRadios[k].addEventListener('change', function () {
2242
+ var byMessage = form.querySelector('input[name="runs"]:checked').value === 'message';
2243
+ messageField.hidden = !byMessage;
2244
+ if (paramsBox) paramsBox.hidden = byMessage;
2245
+ });
2246
+ }
1866
2247
  form.addEventListener('submit', function (ev) {
1867
2248
  ev.preventDefault();
1868
2249
  var data = new FormData(form);
2250
+ var runsWorkflow = data.get('runs') === 'workflow';
2251
+ if (!runsWorkflow && !String(data.get('message') || '').trim()) { result.textContent = 'Write the message the agent receives.'; return; }
1869
2252
  var schedule;
1870
2253
  if (data.get('when') === 'once') {
1871
2254
  if (!data.get('runAt')) { result.textContent = 'Pick a date and time.'; return; }
@@ -1880,7 +2263,10 @@ ${consoleShellScript}
1880
2263
  result.textContent = 'Scheduling' + String.fromCharCode(8230);
1881
2264
  fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/schedules', {
1882
2265
  method: 'POST', headers: { 'content-type': 'application/json' },
1883
- body: JSON.stringify({ message: data.get('message'), schedule: schedule, task_name: data.get('task_name'), notify: data.get('notify') === 'on' }),
2266
+ body: JSON.stringify(Object.assign(
2267
+ runsWorkflow ? { workflow_id: data.get('workflow_id'), params: scheduleParams(data) } : { message: data.get('message') },
2268
+ { schedule: schedule, task_name: data.get('task_name'), notify: data.get('notify') === 'on' }
2269
+ )),
1884
2270
  }).then(function (r) {
1885
2271
  if (!r.ok) { result.textContent = r.body.error || 'Could not schedule'; return; }
1886
2272
  loadSchedulesTab(name);
@@ -1897,6 +2283,7 @@ ${consoleShellScript}
1897
2283
  if (!r.ok) throw new Error(r.body.error || 'request failed');
1898
2284
  var tasks = r.body.tasks || [];
1899
2285
  schedulesTimeZone = r.body.time_zone || null;
2286
+ schedulesWorkflows = r.body.workflows || [];
1900
2287
  content.innerHTML = renderSchedulesHtml(tasks);
1901
2288
  wireSchedulesHandlers(name);
1902
2289
  leShell.setCount('schedules', tasks.filter(function (t) { return t.status !== 'cancelled' && t.status !== 'done'; }).length);