Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ linters:
- third_party$
- builtin$
- ^examples/
- ^sdks/go/examples/
- '(.+)_test\.go'
- "cmd/hatchet-loadtest/rampup/(.+).go"
formatters:
Expand All @@ -71,4 +72,5 @@ formatters:
paths:
- third_party$
- builtin$
- ^examples/
- ^examples/go/
- ^sdks/go/examples/
89 changes: 72 additions & 17 deletions examples/go/scheduled/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,102 @@ import (
"log"
"time"

"github.com/google/uuid"
"github.com/oapi-codegen/runtime/types"

"github.com/hatchet-dev/hatchet/pkg/client/rest"
hatchet "github.com/hatchet-dev/hatchet/sdks/go"
"github.com/hatchet-dev/hatchet/sdks/go/features"
)

type SimpleInput struct {
Message string `json:"message"`
}

type SimpleOutput struct {
Result string `json:"result"`
}

func main() {
client, err := hatchet.NewClient()
if err != nil {
log.Fatalf("failed to create hatchet client: %v", err)
}

simple := client.NewStandaloneTask("simple", func(ctx hatchet.Context, input SimpleInput) (SimpleOutput, error) {
return SimpleOutput{
Result: "Processed: " + input.Message,
}, nil
})

// > Create
scheduledRun, err := client.Schedules().Create(
context.Background(),
"scheduled",
features.CreateScheduledRunTrigger{
TriggerAt: time.Now().Add(1 * time.Minute),
Input: map[string]interface{}{"message": "Hello, World!"},
},
)
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
tomorrowNoon := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 12, 0, 0, 0, time.UTC)

scheduledRun, err := simple.Schedule(context.Background(), tomorrowNoon, SimpleInput{Message: "Hello, World!"})
if err != nil {
log.Fatalf("failed to create scheduled run: %v", err)
}

scheduledRunId := scheduledRun.GetScheduledWorkflows()[0].GetId()
Comment thread
mnafees marked this conversation as resolved.

// > Delete
err = client.Schedules().Delete(
client.Schedules().Delete(
context.Background(),
scheduledRun.Metadata.Id,
scheduledRunId,
)
Comment thread
mnafees marked this conversation as resolved.
if err != nil {
log.Fatalf("failed to delete scheduled run: %v", err)
}

// > List
scheduledRuns, err := client.Schedules().List(
client.Schedules().List(
context.Background(),
rest.WorkflowScheduledListParams{},
)

// > Reschedule
client.Schedules().Update(
context.Background(),
scheduledRunId,
rest.UpdateScheduledWorkflowRunRequest{
TriggerAt: time.Now().UTC().Add(10 * time.Second),
},
)

scheduledRunIds := []types.UUID{types.UUID(uuid.MustParse(scheduledRunId))}

// > Bulk Delete
client.Schedules().BulkDelete(
context.Background(),
rest.ScheduledWorkflowsBulkDeleteRequest{
ScheduledWorkflowRunIds: &scheduledRunIds,
},
)

scheduledRunIdUUID := types.UUID(uuid.MustParse(scheduledRunId))

// > Reschedule
client.Schedules().Update(
context.Background(),
scheduledRunId,
rest.UpdateScheduledWorkflowRunRequest{
TriggerAt: time.Now().UTC().Add(10 * time.Second),
},
)

// > Bulk Update
client.Schedules().BulkUpdate(
context.Background(),
rest.ScheduledWorkflowsBulkUpdateRequest{
Updates: []rest.ScheduledWorkflowsBulkUpdateItem{
{Id: scheduledRunIdUUID, TriggerAt: time.Now().UTC().Add(10 * time.Second)},
},
},
)
Comment thread
mnafees marked this conversation as resolved.

worker, err := client.NewWorker("scheduled-worker", hatchet.WithWorkflows(simple))
if err != nil {
log.Fatalf("failed to list scheduled runs: %v", err)
log.Fatalf("failed to create worker: %v", err)
}

_ = scheduledRuns
if err := worker.StartBlocking(context.Background()); err != nil {
log.Fatalf("failed to start worker: %v", err)
}
}
10 changes: 5 additions & 5 deletions examples/python/simple/schedule.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# > Schedule a Task
from datetime import datetime
from datetime import datetime, timedelta, timezone

from examples.simple.worker import simple

schedule = simple.schedule(datetime(2025, 3, 14, 15, 9, 26))
# > Schedule a Task

tomorrow_noon = datetime.now(tz=timezone.utc).replace(hour=12, minute=0, second=0, microsecond=0) + timedelta(days=1)

## 👀 do something with the id
print(schedule.id)
scheduled_run = simple.schedule(tomorrow_noon, input={"Message": "hello"})

22 changes: 9 additions & 13 deletions examples/typescript/simple/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,27 @@ import { simple } from './workflow';
async function main() {
// > Create a Scheduled Run

const runAt = new Date(new Date().setHours(12, 0, 0, 0) + 24 * 60 * 60 * 1000);
const tomorrowNoon = new Date();
tomorrowNoon.setUTCDate(tomorrowNoon.getUTCDate() + 1);
tomorrowNoon.setUTCHours(12, 0, 0, 0);

const scheduled = await simple.schedule(runAt, {
Message: 'hello',
const scheduled = await simple.schedule(tomorrowNoon, {
Message: 'Hello, World!',
});

// 👀 Get the scheduled run ID of the workflow
// it may be helpful to store the scheduled run ID of the workflow
// in a database or other persistent storage for later use

const scheduledRunId = scheduled.metadata.id;
console.log(scheduledRunId);

Comment thread
mnafees marked this conversation as resolved.
// > Reschedule a Scheduled Run
await hatchet.scheduled.update(scheduledRunId, {
triggerAt: new Date(Date.now() + 60 * 60 * 1000),
triggerAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
});

// > Delete a Scheduled Run
await hatchet.scheduled.delete(scheduled);
await hatchet.scheduled.delete(scheduledRunId);

// > List Scheduled Runs
const scheduledRuns = await hatchet.scheduled.list({
workflow: simple,
});
console.log(scheduledRuns);
const scheduledRuns = await hatchet.scheduled.list({});

// > Bulk Delete Scheduled Runs
await hatchet.scheduled.bulkDelete({
Expand Down
29 changes: 15 additions & 14 deletions frontend/docs/pages/v1/scheduled-runs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import UniversalTabs from "@/components/UniversalTabs";

# Scheduled Runs

> This example assumes we have a [task](/v1/tasks) registered on a running [worker](/v1/workers).
Scheduled runs allow you to trigger a task at a specific time in the future.

Scheduled runs allow you to trigger a task at a specific time in the future. Some example use cases of scheduling runs might include:
Some example use cases of scheduling runs might include:

- Sending a reminder email at a specific time after a user took an action.
- Running a one-time maintenance task at a predetermined time as determined by your application. For instance, you might want to run a database vacuum during a maintenance window any time a task matches a certain criteria.
Expand Down Expand Up @@ -51,7 +51,7 @@ Here's an example of creating a scheduled run to trigger a task tomorrow at noon

In this example you can have different scheduled times for different customers, or dynamically set the scheduled time based on some other business logic.

When creating a scheduled run via the API, you will receive a scheduled run object with a metadata property containing the id of the scheduled run. This id can be used to reference the scheduled run when deleting the scheduled run and is often stored in a database or other persistence layer.
When creating a scheduled run via the API, you will receive a scheduled run object with a metadata property containing the ID of the scheduled run. This ID can be used to reference the scheduled run when deleting the scheduled run and is often stored in a database or other persistence layer.

<Callout type="info">
Note: Be mindful of the time zone of the scheduled run. Scheduled runs are
Expand All @@ -60,8 +60,6 @@ When creating a scheduled run via the API, you will receive a scheduled run obje

### Deleting a Scheduled Run

You can delete a scheduled run by calling the `delete` method on the scheduled client.

<UniversalTabs items={["Python", "Typescript", "Go", "Ruby"]} variant="hidden">
<Tabs.Tab title="Python">
<Snippet src={snippets.python.scheduled.programatic_sync.delete} />
Expand All @@ -79,8 +77,6 @@ You can delete a scheduled run by calling the `delete` method on the scheduled c

### Listing Scheduled Runs

You can list all scheduled runs for a task by calling the `list` method on the scheduled client.

<UniversalTabs items={["Python", "Typescript", "Go", "Ruby"]} variant="hidden">
<Tabs.Tab title="Python">
<Snippet src={snippets.python.scheduled.programatic_sync.list} />
Expand All @@ -98,9 +94,7 @@ You can list all scheduled runs for a task by calling the `list` method on the s

### Rescheduling a Scheduled Run

If you need to change the trigger time for an existing scheduled run, you can reschedule it by updating its `triggerAt`.

<UniversalTabs items={["Python", "Typescript", "Ruby"]} variant="hidden">
<UniversalTabs items={["Python", "Typescript", "Go", "Ruby"]} variant="hidden">
<Tabs.Tab title="Python">
<Snippet src={snippets.python.scheduled.programatic_sync.reschedule} />

Expand All @@ -111,6 +105,9 @@ If you need to change the trigger time for an existing scheduled run, you can re
/>

</Tabs.Tab>
<Tabs.Tab title="Go">
<Snippet src={snippets.go.scheduled.main.reschedule} />
</Tabs.Tab>
<Tabs.Tab title="Ruby">
<Snippet src={snippets.ruby.scheduled.programatic_sync.reschedule} />
</Tabs.Tab>
Expand All @@ -126,18 +123,22 @@ If you need to change the trigger time for an existing scheduled run, you can re

Hatchet supports bulk operations for scheduled runs. You can bulk delete scheduled runs, and you can bulk reschedule scheduled runs by providing a list of updates.

<UniversalTabs items={["Python", "Typescript", "Ruby"]} variant="hidden">
<UniversalTabs items={["Python", "Typescript", "Go", "Ruby"]} variant="hidden">
<Tabs.Tab title="Python">
<Snippet src={snippets.python.scheduled.programatic_sync.bulk_delete} />
<Snippet src={snippets.python.scheduled.programatic_sync.bulk_reschedule} />

</Tabs.Tab>
<Tabs.Tab title="Typescript">
<Snippet src={snippets.typescript.simple.schedule.bulk_delete_scheduled_runs} />
<Snippet
src={snippets.typescript.simple.schedule.bulk_delete_scheduled_runs}
/>
<Snippet
src={snippets.typescript.simple.schedule.bulk_reschedule_scheduled_runs}
/>

</Tabs.Tab>
<Tabs.Tab title="Go">
<Snippet src={snippets.go.scheduled.main.bulk_delete} />
<Snippet src={snippets.go.scheduled.main.bulk_update} />
</Tabs.Tab>
<Tabs.Tab title="Ruby">
<Snippet src={snippets.ruby.scheduled.programatic_sync.bulk_delete} />
Expand Down
10 changes: 10 additions & 0 deletions frontend/docs/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,13 @@ nav:not(.nextra-toc) {
margin-bottom: 0.5rem;
justify-content: flex-start;
}

/* Place language selector before nav links in the navbar.
Nextra navbar flex order: logo(0) → navItems(0,src2) → search(0,src3) → github(0,src4) → discord(0,src5) → extraContent(0,src6) → hamburger(0,src7)
We assign explicit orders so the language selector (order:1) sits before nav items (order:2). */
.nextra-nav-container nav > .nextra-scrollbar { order: 2; }
.nextra-nav-container nav > :nth-child(3) { order: 3; } /* search */
.nextra-nav-container nav > :nth-child(4) { order: 4; } /* github */
.nextra-nav-container nav > :nth-child(5) { order: 5; } /* discord */
.nextra-nav-container nav > .nextra-hamburger { order: 6; }
Comment thread
mnafees marked this conversation as resolved.
.nextra-lang-selector-nav > div { margin-left: 0; }
8 changes: 7 additions & 1 deletion frontend/docs/theme.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ const config = {
/>
</svg>
),
navbar: {
extraContent: (
<div className="nextra-lang-selector-nav" style={{ order: 1, display: "flex", alignItems: "center" }}>
<LanguageSelectorButton />
</div>
),
},
head: () => {
const { title } = useConfig();
const router = useRouter();
Expand Down Expand Up @@ -195,7 +202,6 @@ const config = {
<MarkdownIcon />
<span className="page-action-label">View as MD</span>
</a>
<LanguageSelectorButton />
</div>
{children}
</>
Expand Down
30 changes: 28 additions & 2 deletions pkg/client/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ type AdminClient interface {

ScheduleWorkflow(workflowName string, opts ...ScheduleOptFunc) error

ScheduleWorkflowV1(ctx context.Context, workflowName string, triggerAt time.Time, input any) (*admincontracts.WorkflowVersion, error)
Comment thread
mnafees marked this conversation as resolved.

// RunWorkflow triggers a workflow run and returns the run id
RunWorkflow(workflowName string, input interface{}, opts ...RunOptFunc) (*Workflow, error)

Expand Down Expand Up @@ -116,7 +118,9 @@ type adminClientImpl struct {

ctx *contextLoader

namespace string
namespace string
tenantId string
restClient *rest.ClientWithResponses

subscriber SubscribeClient

Expand All @@ -126,16 +130,18 @@ type adminClientImpl struct {
listener *WorkflowRunsListener
}

func newAdmin(conn *grpc.ClientConn, opts *sharedClientOpts, subscriber SubscribeClient) AdminClient {
func newAdmin(conn *grpc.ClientConn, opts *sharedClientOpts, subscriber SubscribeClient, restClient *rest.ClientWithResponses) AdminClient {
return &adminClientImpl{
client: admincontracts.NewWorkflowServiceClient(conn),
v1Client: v1contracts.NewAdminServiceClient(conn),
l: opts.l,
v: opts.v,
ctx: opts.ctxLoader,
namespace: opts.namespace,
tenantId: opts.tenantId,
subscriber: subscriber,
sharedMeta: opts.sharedMeta,
restClient: restClient,
}
}

Expand Down Expand Up @@ -253,6 +259,26 @@ func (a *adminClientImpl) ScheduleWorkflow(workflowName string, fs ...ScheduleOp
return nil
}

func (a *adminClientImpl) ScheduleWorkflowV1(ctx context.Context, workflowName string, triggerAt time.Time, input any) (*admincontracts.WorkflowVersion, error) {
inputBytes, err := json.Marshal(input)
if err != nil {
return nil, fmt.Errorf("could not marshal input: %w", err)
}

workflowName = client.ApplyNamespace(workflowName, &a.namespace)

resp, err := a.client.ScheduleWorkflow(a.ctx.newContext(ctx), &admincontracts.ScheduleWorkflowRequest{
Name: workflowName,
Schedules: []*timestamppb.Timestamp{timestamppb.New(triggerAt)},
Input: string(inputBytes),
})
Comment thread
mnafees marked this conversation as resolved.
if err != nil {
return nil, fmt.Errorf("could not schedule workflow: %w", err)
}

return resp, nil
}

type RunOptFunc func(*admincontracts.TriggerWorkflowRequest) error

func WithRunMetadata(metadata interface{}) RunOptFunc {
Expand Down
Loading
Loading