+
+ onNavigate("/automations")} className={cn("text-xs hover:text-white", segment ? "text-neutral-500" : "text-neutral-200")}>Automations
+ {segment ? <>{creating ? "New automation" : selected?.name ?? "Automation"} > : null}
+
+ {segment ? <>
onNavigate("/automations")}>Cancel {busy ? : null}{creating ? "Create automation" : "Save changes"} > :
onNavigate("/automations/new")}> }
+
+
+
+ {appsError || missingApps.length > 0 ? (
+
+
+
{appsError ? "Could not verify your Composio connections" : "Connect your apps first"}
+
{appsError ? "Open Connections to check your apps before running this automation." : `Connect ${missingApps.join(", ")} through Composio in Connections ${draft ? "before running this automation" : "to use their automation templates"}.`}
+
+
onNavigate("/connections")}>Connect apps
+
+ ) : null}
+ {error ?
{error} setError(null)}>
: null}
+ {notice ?
{notice}
: null}
+ {loading ?
Loading automations…
: segment && draft && context ? (
+
+
update("name", event.target.value)} className="w-full bg-transparent text-[22px] font-medium tracking-[-0.025em] outline-none focus-visible:ring-1 focus-visible:ring-neutral-500" maxLength={160} />
+
+ {context.organizationName}
+ update("runAsMembershipId", event.target.value)}>
+ {context.members.map((member) => {member.name} )}
+ {!context.members.some((member) => member.id === draft.runAsMembershipId) ? {selected?.runAsName} : null}
+
+
+
+
Post as run-as person
Delegated Slack posting is not supported yet. Results stay in Nova.
+
+
+
setAddingTrigger((value) => !value)}> Add trigger : undefined}>
+ {addingTrigger ? {(Object.keys(TRIGGER_LABELS) as TriggerType[]).map((type) => = 10} onClick={() => { update("triggers", [...draft.triggers, defaultTrigger(type)]); setAddingTrigger(false) }}>{providerFor(type)} · {TRIGGER_LABELS[type]} )}
: null}
+ {draft.triggers.map((trigger, index) => update("triggers", draft.triggers.map((item, position) => position === index ? next : item))} onRemove={() => update("triggers", draft.triggers.filter((_, position) => position !== index))} />)}
+
+
+
+
+
+
Nova drafts answers and PR-ready changes. It does not run shell commands, execute checks, open pull requests, or post externally.
+ {id && selected ? <>
+
+
void action(async () => { const data = await request<{ automation: AutomationDto }>(`/${id}`, "PUT", { ...selected, status: selected.status === "active" ? "paused" : "active" }); setSelected(data.automation); update("status", data.automation.status) })}>{selected.status === "active" ? : }{selected.status === "active" ? "Pause automation" : "Resume automation"}
+
setTestOpen(true)}> Test run
+ {selected.triggers.some((trigger) => trigger.type === "webhook") ?
void action(async () => { setCredential(await request(`/${id}/credential`, "POST")) })}>Generate / rotate webhook token : null}
+
setDeleteOpen(true)}>
+
+
+ {selected.runs.length ? {selected.runs.map((run) =>
{run.status === "filtered" ? "Skipped by criteria" : run.status}
{run.reason ?
{run.reason}
: null}
{new Date(run.createdAt).toLocaleString()}
{run.agentSessionId ?
onNavigate(`/s/${run.agentSessionId}`)}>Open thread → : null}
)}
: No runs yet
}
+
+ > : null}
+
+ ) : !segment ? (
+
+
+ {[{ id: "mine", label: "My automations" }, { id: "all", label: "All automations" }, { id: "scheduled", label: "Scheduled" }].map((item) =>
setTab(item.id)}>{item.label} )}
+
setQuery(event.target.value)} className="w-24 bg-transparent text-xs outline-none focus-visible:ring-1 focus-visible:ring-neutral-500 sm:w-40" placeholder="Search" /> setFiltersOpen((value) => !value)}>
+
+
+ {tab === "mine" ? setTab("all")}>Creator is Me : null}
+ {filtersOpen ? setStatusFilter(event.target.value)}>All statuses Active Paused : null}
+
+ {filtered.length ?
{filtered.map((item) =>
onNavigate(`/automations/${item.id}`)}>{item.name}
{item.creatorName} · {item.triggers.map((trigger) => TRIGGER_LABELS[trigger.type]).join(", ")}
{item.status} )}
:
{query || statusFilter !== "all" ? "No matching automations" : "No automations yet"}
}
+
Templates {AUTOMATION_TEMPLATES.length}
+
{AUTOMATION_TEMPLATES.map((template) =>
onNavigate(`/automations/new?template=${template.id}`)} className="min-h-[104px] rounded-lg border border-white/10 bg-white/[0.02] px-4 py-3.5 text-left transition-colors hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-neutral-400"> {template.name}{template.description} {template.provider} {template.comingSoon ? Coming soon : null} )}
+
+ ) : null}
+
+ {deleteOpen && id ?
setDeleteOpen(false)}>This deletes the automation and its run history. Existing Nova threads will remain.
setDeleteOpen(false)}>Cancel void action(async () => { await request(`/${id}`, "DELETE"); onNavigate("/automations") })}>Delete automation
: null}
+ {testOpen && id ?
setTestOpen(false)}>Provide sample event context. The saved instructions, criteria, limits, and model will be used.
: null}
+ {credential ?
setCredential(null)}>Shown only once. Store it securely. Rotating invalidates the previous token. Send it in the Authorization: Bearer header, with a unique X-Delivery-Id for each event. Never include secrets in the event body.
{typeof window === "undefined" ? "" : window.location.origin}{credential.path}
{ void navigator.clipboard.writeText(credential.token).then(() => { setCredential(null); setNotice("Token copied. Store it securely.") }).catch(() => setError("Could not copy the token")) }}>Copy token and close : null}
+
+ )
+}
+
+function Section({ title, description, action, children }: { title: string; description: string; action?: ReactNode; children: ReactNode }) {
+ return
+
{TRIGGER_LABELS[trigger.type]}
+ {trigger.type === "slack.message" ? <>
onChange({ ...trigger, actor: event.target.value })} />
onChange({ ...trigger, channel: event.target.value })} />
onChange({ ...trigger, humansOnly: event.target.checked })} />humans only> : null}
+ {trigger.type === "github.check" || trigger.type === "github.review" ?
onChange({ ...trigger, repository: event.target.value })}>in any repository {context.repositories.map((repo) => {repo.fullName} )} : null}
+ {trigger.type === "github.review" ?
onChange({ ...trigger, actor: event.target.value })} /> : null}
+ {trigger.type === "linear.issue" ?
onChange({ ...trigger, team: event.target.value })} /> : null}
+ {trigger.type === "schedule" ? <>
onChange({ ...trigger, day: Number(event.target.value) })}>{["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].map((day, index) => {day} )} { const [hour, minute] = event.target.value.split(":").map(Number); onChange({ ...trigger, hour, minute }) }} />
onChange({ ...trigger, timezone: event.target.value })} />> : null}
+
+
+ {trigger.type === "github.check" ?
and with conclusion {(["failure", "timed_out", "action_required", "success", "cancelled", "neutral", "skipped", "stale"] as const).map((value) => onChange({ ...trigger, conclusions: event.target.checked ? [...trigger.conclusions, value] : trigger.conclusions.filter((item) => item !== value) })} />{value.replaceAll("_", " ")} )}
: null}
+ {trigger.type === "github.review" ?
and with review state {(["changes_requested", "commented", "approved"] as const).map((value) => onChange({ ...trigger, states: event.target.checked ? [...trigger.states, value] : trigger.states.filter((item) => item !== value) })} />{value.replaceAll("_", " ")} )}
: null}
+ {trigger.type === "webhook" ?
Save the automation, then generate a secure webhook token.
: null}
+
+}
+
+function Modal({ title, onClose, children }: { title: string; onClose: () => void; children: ReactNode }) {
+ return