Skip to content

AI-First Starting UX: Build from scratch#4918

Draft
lmac-1 wants to merge 11 commits into
4848-ai-first-starting-ux-parentfrom
4895-ai-first-build-from-scratch
Draft

AI-First Starting UX: Build from scratch#4918
lmac-1 wants to merge 11 commits into
4848-ai-first-starting-ux-parentfrom
4895-ai-first-build-from-scratch

Conversation

@lmac-1

@lmac-1 lmac-1 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Adds the "Build from scratch" path from the new-workflow landing screen (part of the #4848 AI-first starting UX epic).

Clicking the "Build from scratch" card now:

  1. Immediately creates and saves a workflow via a new Workflows.create_webhook_workflow/2: a webhook trigger, one "Transform Data" job on @openfn/language-common@latest, and an always-enabled edge — the same shape as the old "Event-based template".
  2. Navigates to the canvas with ?trigger=<id> in the URL, so the trigger inspector ("What triggers this workflow?") is open on load.
  3. Because the workflow is already persisted, the canvas opens in the normal saved state: no pink unsaved toolbar, Save button visible.

The build_from_scratch LiveView event is authorized with the role-scoped ProjectUsers :create_workflow policy, so viewers get a permission error flash instead of a new workflow.

Workflows are named "Untitled workflow", "Untitled workflow 1", etc. via a new shared Workflows.unique_workflow_name/2 — extracted from the identical private copies in workflow_channel.ex and new_workflow_component.ex, and now used by both this flow and the channel's validate_workflow_name. (The legacy component keeps its copy since that path is being deleted in this epic.) This also fixes a bug where clicking the card a second time in the same project failed on the name unique constraint.

The card is also protected against double-clicks: useActionLock ensures one build_from_scratch event per click-burst, the card is disabled while creation is pending, and the LiveView handler has a creating_workflow? guard as a server-side backstop.

Closes #4895

Additional changes

Workflow activation limiter: create_webhook_workflow/2 now calls WorkflowUsageLimiter.limit_workflow_creation/1 before saving. If a project is at its active-workflow quota, the trigger is created with enabled: false — matching the auto-disable behavior the collab-editor session path already applies to new workflows at the limit. Without this, a project at quota could bypass it by using the "Build from scratch" entry point.

Browser history fix: The redirect from the landing screen to the canvas now uses replace: true on push_navigate, so the canvas URL replaces the /new entry in the browser history. Previously, hitting Back from the canvas would return to the landing screen rather than wherever the user navigated from before. The one-shot ?trigger_view=picker param is also cleaned up with replace: true in updateSearchParams, for the same reason.

Validation steps

  1. In a project, open the new collaborative workflow screen — the landing screen appears.
  2. Click Build from scratch.
  3. You should land on the canvas of a saved workflow named "Untitled workflow" with a webhook trigger connected to a "Transform Data" job. The trigger inspector should be open, and the toolbar should be in the normal saved state.
  4. Go back to the workflow list — the workflow was persisted.
  5. Repeat steps 1–2 in the same project: the second workflow should be created as "Untitled workflow 1" instead of failing.
  6. Delete one of them and build from scratch again: the freed name is reused (deletion renames the old row to <name>_del).
  7. Double-click the card rapidly: only one workflow should be created, and the card should grey out while creation is pending.
  8. Log in as a project viewer and click Build from scratch: you should see "You don't have permission to create workflows" and no workflow should be created.
  9. Press Back from the canvas after building from scratch — you should return to wherever you were before the landing screen, not back to the landing screen itself.

Additional notes for the reviewer

  1. create_webhook_workflow/2 goes through the existing save_workflow/2 pipeline, so snapshots, lock_version, and audit behave like any other workflow save.
  2. Failure to save shows a "Failed to create workflow" flash and keeps the user on the landing screen.
    1. The limiter fix only has a visible effect on the Thunderbolt SaaS build (the open-source stub always returns :ok). On SaaS, a project at its quota now gets a disabled trigger instead of an enabled one.

AI Usage

Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):

  • I have used Claude Code
  • I have used another model
  • I have not used AI

You can read more details in our
Responsible AI Policy

Pre-submission checklist

  • I have performed an AI review of my code (we recommend using /review
    with Claude Code)
  • I have implemented and tested all related authorization policies.
    (e.g., :owner, :admin, :editor, :viewer)
  • I have updated the changelog.
  • I have ticked a box in "AI usage" in this PR

@github-project-automation github-project-automation Bot moved this to New Issues in Core Jul 2, 2026
Base automatically changed from 4868-build-with-ai to 4848-ai-first-starting-ux-parent July 6, 2026 08:17
Adds the "Build from scratch" entry point on the new-workflow landing
screen: Workflows.create_webhook_workflow/2 builds a workflow with a
webhook trigger, one job, and an always-on edge; the build_from_scratch
LiveView event authorizes via the role-scoped ProjectUsers
:create_workflow check so viewers can't trigger it.
@lmac-1 lmac-1 force-pushed the 4895-ai-first-build-from-scratch branch from 0110e36 to 290612b Compare July 6, 2026 09:05
lmac-1 added 10 commits July 6, 2026 12:54
…flow_name/2

Clicking "Build from scratch" twice in one project failed on the workflow
name unique constraint, since create_webhook_workflow/2 hardcoded its name.

Extract the collision-avoidance logic duplicated in workflow_channel.ex and
new_workflow_component.ex into a public Workflows.unique_workflow_name/2
(names-only query, includes soft-deleted rows since the unique index is not
partial) and use it in create_webhook_workflow/2, which now defaults to
"Untitled workflow" with " 1", " 2"... suffixes on collision.

The channel delegates to the shared function. The legacy component keeps its
private copy because that path is being deleted in this epic.
Rapid double-clicks on the "Build from scratch" card sent multiple
build_from_scratch events, each creating a workflow. Wrap the push in
useActionLock so only one event goes out, disable the card while pending,
and guard the LiveView handler with a creating_workflow? assign as a
server-side backstop for events that arrive anyway.

Also normalize the new-workflow placeholder to "Untitled workflow" to match
the rest of the codebase, and add regression tests: repeated clicks create
suffixed workflows, creation after deletion reuses the freed name, and a
failed save shows an error flash.
Build-from-scratch now redirects with ?trigger_view=picker so the
newly created webhook trigger opens on the "What triggers this
workflow?" picker instead of the read-only show panel, inviting the
user to actively choose a trigger type.

TriggerInspector captures the one-shot signal via a lazy useState
initializer, then clears it from both React state (so a later Edit
click lands on Choose, not the picker) and the URL via
updateSearchParams (so a page refresh doesn't reopen the picker).
updateSearchParams always pushed a history entry, so stripping the one-shot ?trigger_view=picker signal after build-from-scratch left a phantom Back-button stop pointing right back at the picker. Add an optional { replace: true } to patch the current entry in place instead, and use it in TriggerInspector.
…nvas

The build_from_scratch redirect pushed a new history entry on top of /new, so Back from the canvas landed on the now-pointless landing screen instead of wherever the user was before it. Pass replace: true to push_navigate so the canvas URL takes over /new's slot.
…limit

create_webhook_workflow/2 was calling save_workflow/2 with enabled: true
unconditionally, bypassing the WorkflowUsageLimiter. A project at its
active-workflow quota could use the Build from scratch entry point to
create a live webhook trigger regardless of quota.

Now calls limit_workflow_creation/1 before building attrs and sets
trigger enabled: false if the limit is exceeded, mirroring the
auto-disable behaviour the collab-editor session path already applies
to new workflows at the limit.
…toast

Wraps the build_from_scratch pushEventTo call in a 10s timeout so the
card doesn't stay permanently disabled if the server never acknowledges
the push (socket disconnect or server-side error). Shows an error toast
on failure so the user knows to retry.

Also fixes disabled card hover styles bleeding through, renames the
default job to "Untitled job", and removes a stale comment.
save_workflow's @SPEC only listed changeset and :workflow_deleted as
error shapes, but handle_save_result can also return {:error, false}
(snapshot failure) or other Multi step reasons. create_webhook_workflow
inherited the same too-narrow spec.

Widen both specs to match what the code actually returns, and rename
the bound error variables from `changeset` to `reason` since they
aren't always changesets.
The build_from_scratch handler is fully synchronous, so a second
event on the same socket can never observe creating_workflow?: true
— the flag is always reset or the socket has already navigated away
by the time the process could dequeue another event. The client-side
useActionLock is the guard that actually matters. Drop the dead
assign, guard clause, and the test that only exercised it via a
hand-built socket.
useAIWorkflowApplications.globalStep.test.ts was missing the
streamingApply/streamingApplyActions props that its sibling test
files (autoApply, jobCode, workflow) already pass. The hook calls
streamingApplyActions.clear() unconditionally in handleApplyWorkflow,
so leaving it undefined threw a TypeError as soon as that path ran.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New Issues

Development

Successfully merging this pull request may close these issues.

1 participant