diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py
index 9aa808c72..fcb133a90 100644
--- a/backend/open_webui/models/automations.py
+++ b/backend/open_webui/models/automations.py
@@ -57,10 +57,16 @@ class AutomationRun(Base):
####################
+class AutomationTerminalConfig(BaseModel):
+ server_id: str
+ cwd: Optional[str] = None
+
+
class AutomationData(BaseModel):
prompt: str
model_id: str
rrule: str
+ terminal: Optional[AutomationTerminalConfig] = None
class AutomationModel(BaseModel):
diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py
index 06822d563..94d6178ca 100644
--- a/backend/open_webui/routers/automations.py
+++ b/backend/open_webui/routers/automations.py
@@ -91,6 +91,15 @@ async def create_new_automation(
detail=str(e),
)
+ # Validate terminal server exists if linked
+ if form_data.data.terminal and form_data.data.terminal.server_id:
+ connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
+ if not any(c.get('id') == form_data.data.terminal.server_id for c in connections):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail='Terminal server not found',
+ )
+
tz = user.timezone
automation = Automations.insert(
user.id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db
@@ -139,6 +148,15 @@ async def update_automation_by_id(
detail=str(e),
)
+ # Validate terminal server exists if linked
+ if form_data.data.terminal and form_data.data.terminal.server_id:
+ connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
+ if not any(c.get('id') == form_data.data.terminal.server_id for c in connections):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail='Terminal server not found',
+ )
+
tz = user.timezone
updated = Automations.update_by_id(
id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db
diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py
index ba2d75582..5fa67744e 100644
--- a/backend/open_webui/utils/automations.py
+++ b/backend/open_webui/utils/automations.py
@@ -211,6 +211,66 @@ def _resolve_model_filter_ids(app, model_id: str) -> list[str]:
return list(filter_ids) if filter_ids else []
+async def _set_terminal_cwd(
+ app, server_id: str, user, cwd: str, chat_id: str
+) -> None:
+ """Set the working directory on a terminal server via the proxy.
+
+ Routes through the open-webui terminal proxy endpoint so that
+ auth headers, orchestrator policy routing, and X-User-Id are
+ handled correctly — same path the frontend uses.
+ """
+ import aiohttp
+
+ connections = getattr(
+ getattr(app, 'state', None), 'config', None
+ )
+ if connections is None:
+ return
+ connections = getattr(connections, 'TERMINAL_SERVER_CONNECTIONS', None) or []
+ connection = next((c for c in connections if c.get('id') == server_id), None)
+ if connection is None:
+ log.warning(f'Terminal server {server_id} not found for CWD set')
+ return
+
+ base_url = (connection.get('url') or '').rstrip('/')
+ if not base_url:
+ return
+
+ # Build target URL — route through orchestrator policy if configured
+ policy_id = connection.get('policy_id')
+ if connection.get('server_type') == 'orchestrator' and policy_id:
+ target_url = f'{base_url}/p/{policy_id}/files/cwd'
+ else:
+ target_url = f'{base_url}/files/cwd'
+
+ headers = {'Content-Type': 'application/json', 'X-User-Id': user.id}
+ if chat_id:
+ headers['X-Session-Id'] = chat_id
+
+ auth_type = connection.get('auth_type', 'bearer')
+ if auth_type == 'bearer':
+ headers['Authorization'] = f'Bearer {connection.get("key", "")}'
+
+ try:
+ async with aiohttp.ClientSession(
+ timeout=aiohttp.ClientTimeout(total=10)
+ ) as session:
+ async with session.post(
+ target_url,
+ json={'path': cwd},
+ headers=headers,
+ ) as resp:
+ if resp.status != 200:
+ body = await resp.text()
+ log.warning(
+ f'Failed to set terminal CWD to {cwd}: '
+ f'HTTP {resp.status} — {body[:200]}'
+ )
+ except Exception as e:
+ log.warning(f'Failed to set terminal CWD: {e}')
+
+
async def execute_automation(app, automation: AutomationModel) -> None:
"""Execute an automation through the full chat completion pipeline.
@@ -226,6 +286,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
prompt = prompt_template(automation.data["prompt"], user)
model_id = automation.data["model_id"]
+ terminal_config = automation.data.get("terminal")
# Generate proper UUIDs for messages (same as frontend)
user_msg_id = str(uuid4())
@@ -291,6 +352,14 @@ async def execute_automation(app, automation: AutomationModel) -> None:
features = _resolve_model_features(app, model_id)
filter_ids = _resolve_model_filter_ids(app, model_id)
+ # If a terminal is linked, set the CWD before building the payload
+ terminal_id = None
+ if terminal_config and terminal_config.get("server_id"):
+ terminal_id = terminal_config["server_id"]
+ cwd = terminal_config.get("cwd")
+ if cwd:
+ await _set_terminal_cwd(app, terminal_id, user, cwd, chat.id)
+
# Build the same payload the frontend sends to /api/chat/completions
form_data = {
"model": model_id,
@@ -308,6 +377,8 @@ async def execute_automation(app, automation: AutomationModel) -> None:
form_data["features"] = features
if filter_ids:
form_data["filter_ids"] = filter_ids
+ if terminal_id:
+ form_data["terminal_id"] = terminal_id
# Call the full chat completion pipeline (same as POST /api/chat/completions).
# The handler reference is stored on app.state to avoid circular imports.
diff --git a/src/lib/apis/automations/index.ts b/src/lib/apis/automations/index.ts
index 5974823d6..30aeb85a1 100644
--- a/src/lib/apis/automations/index.ts
+++ b/src/lib/apis/automations/index.ts
@@ -1,9 +1,15 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
+export type AutomationTerminalConfig = {
+ server_id: string;
+ cwd?: string;
+};
+
export type AutomationData = {
prompt: string;
model_id: string;
rrule: string;
+ terminal?: AutomationTerminalConfig;
};
export type AutomationForm = {
diff --git a/src/lib/components/AutomationModal.svelte b/src/lib/components/AutomationModal.svelte
index 133e67c7e..606d13271 100644
--- a/src/lib/components/AutomationModal.svelte
+++ b/src/lib/components/AutomationModal.svelte
@@ -8,7 +8,7 @@
import XMark from '$lib/components/icons/XMark.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Search from '$lib/components/icons/Search.svelte';
- import Check from '$lib/components/icons/Check.svelte';
+ import Cloud from '$lib/components/icons/Cloud.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
@@ -18,6 +18,7 @@
type AutomationForm,
type AutomationResponse
} from '$lib/apis/automations';
+ import { getTerminalServers, type TerminalServer } from '$lib/apis/terminal/index';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
@@ -42,9 +43,19 @@
let loading = false;
let showScheduleDropdown = false;
let showModelDropdown = false;
+ let showTerminalDropdown = false;
let modelSearch = '';
let customRrule = '';
+ // Terminal state
+ let terminalServers: TerminalServer[] = [];
+ let terminalServerId = '';
+ let terminalCwd = '';
+
+ $: terminalLabel = terminalServerId
+ ? terminalServers.find((s) => s.id === terminalServerId)?.name || 'Terminal'
+ : $i18n.t('Terminal');
+
$: modelLabel = model_id
? $models.find((m) => m.id === model_id)?.name || model_id
: $i18n.t('Select model');
@@ -198,7 +209,15 @@
data: {
prompt: prompt.trim(),
model_id: model_id.trim(),
- rrule: buildRrule()
+ rrule: buildRrule(),
+ ...(terminalServerId
+ ? {
+ terminal: {
+ server_id: terminalServerId,
+ ...(terminalCwd.trim() ? { cwd: terminalCwd.trim() } : {})
+ }
+ }
+ : {})
},
is_active
};
@@ -219,13 +238,22 @@
}
};
- const init = () => {
+ const init = async () => {
+ // Load terminal servers
+ try {
+ terminalServers = await getTerminalServers(localStorage.token);
+ } catch {
+ terminalServers = [];
+ }
+
if (automation) {
name = automation.name;
prompt = automation.data.prompt;
model_id = automation.data.model_id;
is_active = automation.is_active;
parseRrule(automation.data.rrule);
+ terminalServerId = automation.data.terminal?.server_id || '';
+ terminalCwd = automation.data.terminal?.cwd || '';
} else {
name = '';
prompt = '';
@@ -239,8 +267,11 @@
minute = 0;
selectedDays = [];
monthDay = 1;
+ terminalServerId = '';
+ terminalCwd = '';
}
showScheduleDropdown = false;
+ showTerminalDropdown = false;
};
$: if (show) {
@@ -513,6 +544,99 @@
+
+
+ {#if terminalServers.length > 0}
+